skills/api-first/SKILL.md
Use this skill whenever the user needs to scaffold a new REST endpoint or API resource from an OpenAPI 3.1 spec in a Spring Boot 4 project — generating controller + service + repository + DTOs + MapStruct mappers + validation + exception handling + OpenAPI spec in one shot. ALWAYS trigger on: "create endpoint", "add API", "new REST endpoint", "scaffold controller", "API from OpenAPI", "generate controller", "implement API endpoint", "add CRUD endpoint", "create resource". Implicit triggers: user pastes an OpenAPI snippet and asks to "implement it", user describes a new resource with CRUD operations, user mentions REST verbs (POST/GET/PUT/PATCH/DELETE) on a noun resource, user asks for "the endpoint for X". Complements the thinner `api-design` skill which is planning-only — this one writes code. Stack: Java 25 + Spring Boot 4 + Spring Data JDBC (primary) or Spring Data JPA (legacy) + MapStruct 1.6+ + Jakarta Validation + SpringDoc OpenAPI. Encodes the user's API conventions: RFC 9457 Problem Details for errors, response envelope `{data, meta}`, cursor pagination, Idempotency-Key on POST, X-RateLimit-* headers, UUID v7 IDs, ISO 8601 timestamps, money as `{amount: "150.00", currency: "USD"}` string, kebab-case plural resource paths (`/api/v1/payment-links`). Produces compilable code, Testcontainers integration tests, OpenAPI 3.1 spec file, and controller-level slice tests via `@WebMvcTest`.
npx skillsauth add OmexIT/claude-skills-pack api-firstInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Turns an OpenAPI 3.1 fragment (or a short resource spec) into a complete, compilable Spring Boot 4 endpoint stack: controller, service, repository, DTO, mapper, validation, exception handler, OpenAPI spec file, slice tests, and integration tests.
This skill generates a multi-file resource stack. Run it through the superpowers development workflow for reliable output.
@WebMvcTest slice test FIRST (asserting 201 on create, 400 on invalid, 404 on missing, idempotency-key replay), then the integration test with Testcontainers. Then implement../gradlew compileJava or ./mvnw compile), run slice tests, run integration tests. Paste output. Hit the live endpoint with curl and paste the envelope response. Do not claim done without these.Three input modes:
/api-first docs/api/payment-links.yaml/api-first PaymentLink create,list,get,update,deleteAlways confirm shape with the user before generating:
📦 API SCAFFOLD PLAN
Resource: PaymentLink
Path: /api/v1/payment-links
Operations: POST, GET /{id}, GET (list+paginate), PATCH /{id}, DELETE /{id}
Fields: id (UUID v7), amount (money), currency, description, expires_at (Instant), status (enum), created_at, updated_at
Persistence: Spring Data JDBC (detected from build.gradle)
Validation: Jakarta @NotNull, @Positive, @Size
Response: { data, meta: {requestId, timestamp, traceId} }
Errors: RFC 9457 Problem Details
Idempotency: POST accepts Idempotency-Key header
Pagination: cursor-based
Proceeding to generation...
/api/v1/payment-links/api/v1/...){
"data": { "...": "resource" },
"meta": {
"requestId": "req_01HXXX",
"timestamp": "2026-04-15T10:00:00Z",
"traceId": "a1b2c3d4..."
}
}
List responses add pagination metadata:
{
"data": [ "..." ],
"meta": {
"requestId": "req_01HXXX",
"timestamp": "2026-04-15T10:00:00Z",
"traceId": "a1b2c3d4...",
"pagination": {
"cursor": "eyJpZCI6IjAxSFhYWCJ9",
"nextCursor": "eyJpZCI6IjAxSFlZWSJ9",
"hasMore": true,
"pageSize": 20
}
}
}
{
"type": "https://errors.kifiya.com/validation-failed",
"title": "Validation failed",
"status": 400,
"detail": "One or more fields failed validation",
"instance": "/api/v1/payment-links",
"errors": [
{ "field": "amount", "code": "NEGATIVE", "message": "amount must be positive" }
],
"traceId": "a1b2c3d4..."
}
{ "amount": "150.00", "currency": "USD" }
Never a float. Always 2-decimal string (fiat) or 8-decimal string (crypto). Parsed into BigDecimal server-side.
Instant / OffsetDateTime, serialized as ISO 8601 UTC: 2026-04-15T10:00:00.000ZFor a single resource PaymentLink, generate exactly these files:
src/main/java/.../paymentlink/
├── api/
│ ├── PaymentLinkController.java @RestController
│ ├── PaymentLinkRequest.java create/update DTO (record)
│ ├── PaymentLinkResponse.java response DTO (record)
│ └── PaymentLinkListResponse.java list wrapper (record)
├── application/
│ ├── PaymentLinkService.java @Service
│ ├── PaymentLinkMapper.java @Mapper (MapStruct)
│ └── PaymentLinkNotFoundException.java sealed exception
├── domain/
│ └── PaymentLink.java aggregate root (record for JDBC)
├── infrastructure/
│ └── PaymentLinkRepository.java Spring Data JDBC interface
└── config/
└── (package-private — shared with other resources)
src/main/resources/db/changelog/changes/sql/
└── payment-link-001-schema.sql Liquibase
src/test/java/.../paymentlink/
├── PaymentLinkControllerTest.java @WebMvcTest slice
└── PaymentLinkServiceTest.java Mockito unit
src/test-integration/java/.../paymentlink/
└── PaymentLinkIntegrationTest.java @SpringBootTest + Testcontainers
src/main/resources/openapi/
└── payment-links.yaml OpenAPI 3.1 fragment
@RestController
@RequestMapping("/api/v1/payment-links")
@Validated
public class PaymentLinkController {
private final PaymentLinkService service;
private final PaymentLinkMapper mapper;
public PaymentLinkController(PaymentLinkService service, PaymentLinkMapper mapper) {
this.service = service;
this.mapper = mapper;
}
@PostMapping
@Operation(summary = "Create a payment link", responses = {
@ApiResponse(responseCode = "201", description = "Created"),
@ApiResponse(responseCode = "400", description = "Validation failed", content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
@ApiResponse(responseCode = "409", description = "Idempotent replay")
})
public ResponseEntity<ApiResponse<PaymentLinkResponse>> create(
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
@RequestBody @Valid PaymentLinkRequest request
) {
var created = service.create(mapper.toDomain(request), idempotencyKey);
var response = mapper.toResponse(created);
return ResponseEntity
.created(URI.create("/api/v1/payment-links/" + created.id()))
.body(ApiResponse.ok(response));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<PaymentLinkResponse>> get(@PathVariable String id) {
return ResponseEntity.ok(ApiResponse.ok(mapper.toResponse(service.getById(id))));
}
@GetMapping
public ResponseEntity<ApiResponse<List<PaymentLinkResponse>>> list(
@RequestParam(required = false) String cursor,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int pageSize
) {
var page = service.list(cursor, pageSize);
var items = page.items().stream().map(mapper::toResponse).toList();
return ResponseEntity.ok(ApiResponse.ofPage(items, page.cursor(), page.nextCursor(), page.hasMore(), pageSize));
}
@PatchMapping("/{id}")
public ResponseEntity<ApiResponse<PaymentLinkResponse>> update(
@PathVariable String id,
@RequestBody @Valid PaymentLinkRequest request
) {
var updated = service.update(id, mapper.toDomain(request));
return ResponseEntity.ok(ApiResponse.ok(mapper.toResponse(updated)));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}
@Service
@Transactional
public class PaymentLinkService {
private final PaymentLinkRepository repository;
private final IdempotencyStore idempotency;
private final Tsid tsid;
public PaymentLinkService(PaymentLinkRepository repository, IdempotencyStore idempotency, Tsid tsid) {
this.repository = repository;
this.idempotency = idempotency;
this.tsid = tsid;
}
public PaymentLink create(PaymentLink toCreate, String idempotencyKey) {
if (idempotencyKey != null) {
return idempotency.executeOnce("payment-link:create:" + idempotencyKey,
() -> doCreate(toCreate));
}
return doCreate(toCreate);
}
private PaymentLink doCreate(PaymentLink toCreate) {
var withDefaults = toCreate.withId(tsid.next())
.withCreatedAt(Instant.now())
.withUpdatedAt(Instant.now())
.withStatus(PaymentLinkStatus.ACTIVE);
return repository.save(withDefaults);
}
@Transactional(readOnly = true)
public PaymentLink getById(String id) {
return repository.findById(id)
.orElseThrow(() -> new PaymentLinkNotFoundException(id));
}
@Transactional(readOnly = true)
public CursorPage<PaymentLink> list(String cursor, int pageSize) {
return repository.findPage(cursor, pageSize);
}
public PaymentLink update(String id, PaymentLink patch) {
var existing = getById(id);
var updated = existing
.withDescription(patch.description())
.withUpdatedAt(Instant.now());
return repository.save(updated);
}
public void delete(String id) {
if (!repository.existsById(id)) throw new PaymentLinkNotFoundException(id);
repository.deleteById(id);
}
}
public record PaymentLinkRequest(
@NotNull @Positive BigDecimal amount,
@NotBlank @Size(min = 3, max = 3) String currency,
@NotBlank @Size(max = 255) String description,
@NotNull @Future Instant expiresAt
) {}
public record PaymentLinkResponse(
String id,
Money amount,
String description,
Instant expiresAt,
String status,
Instant createdAt,
Instant updatedAt
) {
public record Money(String amount, String currency) {}
}
@Mapper(componentModel = "spring", imports = {PaymentLinkResponse.Money.class})
public interface PaymentLinkMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "status", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
PaymentLink toDomain(PaymentLinkRequest request);
@Mapping(target = "amount", expression = "java(new PaymentLinkResponse.Money(link.amount().setScale(2, java.math.RoundingMode.HALF_UP).toPlainString(), link.currency()))")
@Mapping(target = "status", expression = "java(link.status().name())")
PaymentLinkResponse toResponse(PaymentLink link);
}
@Repository
public interface PaymentLinkRepository extends ListCrudRepository<PaymentLink, String> {
@Query("""
SELECT * FROM payment_links
WHERE (:cursor IS NULL OR id < :cursor)
ORDER BY id DESC
LIMIT :pageSize
""")
List<PaymentLink> findPage(@Param("cursor") String cursor, @Param("pageSize") int pageSize);
default CursorPage<PaymentLink> findPage(String cursor, int pageSize) {
var items = findPage(cursor, pageSize + 1);
var hasMore = items.size() > pageSize;
var trimmed = hasMore ? items.subList(0, pageSize) : items;
var nextCursor = hasMore ? trimmed.get(trimmed.size() - 1).id() : null;
return new CursorPage<>(trimmed, cursor, nextCursor, hasMore);
}
}
If no global @ControllerAdvice exists, generate one. If one exists, add a handler for PaymentLinkNotFoundException:
@ExceptionHandler(PaymentLinkNotFoundException.class)
public ResponseEntity<ProblemDetail> handleNotFound(PaymentLinkNotFoundException e) {
var problem = ProblemDetail.forStatus(404);
problem.setType(URI.create("https://errors.kifiya.com/payment-link-not-found"));
problem.setTitle("Payment link not found");
problem.setDetail(e.getMessage());
return ResponseEntity.status(404).body(problem);
}
@WebMvcTest)@WebMvcTest(PaymentLinkController.class)
class PaymentLinkControllerTest {
@Autowired private MockMvc mvc;
@Autowired private ObjectMapper mapper;
@MockBean private PaymentLinkService service;
@MockBean private PaymentLinkMapper linkMapper;
@Test
void should_return_201_when_creating_valid_payment_link() throws Exception {
// Given
var request = """
{"amount":"150.00","currency":"USD","description":"Test","expiresAt":"2099-01-01T00:00:00Z"}
""";
given(service.create(any(), any())).willReturn(aPaymentLink());
given(linkMapper.toDomain(any())).willReturn(aPaymentLink());
given(linkMapper.toResponse(any())).willReturn(aResponse());
// When / Then
mvc.perform(post("/api/v1/payment-links")
.header("Idempotency-Key", "key-1")
.contentType(MediaType.APPLICATION_JSON)
.content(request))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.id").exists())
.andExpect(header().string("Location", startsWith("/api/v1/payment-links/")));
}
@Test
void should_return_400_when_amount_is_negative() throws Exception {
mvc.perform(post("/api/v1/payment-links")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"amount":"-10","currency":"USD","description":"x","expiresAt":"2099-01-01T00:00:00Z"}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[?(@.field=='amount')].code").value("NEGATIVE"));
}
}
openapi: 3.1.0
info:
title: Payment Links API
version: 1.0.0
paths:
/api/v1/payment-links:
post:
summary: Create a payment link
parameters:
- name: Idempotency-Key
in: header
required: false
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentLinkRequest' }
responses:
'201':
description: Created
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentLinkResponseEnvelope' }
'400': { $ref: '#/components/responses/ValidationFailed' }
'409': { $ref: '#/components/responses/Conflict' }
components:
schemas:
PaymentLinkRequest:
type: object
required: [amount, currency, description, expiresAt]
properties:
amount: { type: string, pattern: '^[0-9]+(\.[0-9]{1,2})?$' }
currency: { type: string, minLength: 3, maxLength: 3 }
description: { type: string, maxLength: 255 }
expiresAt: { type: string, format: date-time }
produces:
- type: "code"
format: "java"
paths: [<list above>]
- type: "migration"
format: "sql"
paths: ["src/main/resources/db/changelog/changes/sql/<resource>-001-schema.sql"]
- type: "openapi"
format: "yaml"
paths: ["src/main/resources/openapi/<resource>.yaml"]
- type: "test"
format: "java"
paths: [<list above>]
handoff: "Write claudedocs/handoff-api-first-<timestamp>.yaml — suggest: verify-impl, db-migration (if schema changes)"
RestTemplate in generated clients — use RestClient (Spring 6.1+)@Autowired on fields) — constructor onlyString.format — use .formatted() (Java 15+)double/float for money — always BigDecimaljava.util.Date — always Instant / OffsetDateTime@Transactional(readOnly = false) explicit — omit; readOnly=false is default@ControllerAdvicetools
Use this skill to verify a completed implementation through live testing — API calls, database state checks, and UI automation with Playwright. Triggers include: "test the implementation", "verify this works", "run API tests", "check the database", "test the UI", "end-to-end verify", "smoke test", "sanity check the implementation", "manually test", or any time an implementation needs post-build validation beyond unit tests. Also triggered automatically by spec-to-impl during the integration review phase. Use this when you want real evidence the system works — not just that tests compile. Can consume a pre-generated e2e/test-plan.yaml from spec-to-impl for fully automated test execution.
development
--- name: ux-review description: Evaluate a UI/UX design or implementation using heuristic analysis, accessibility audit, and cognitive walkthrough. Triggers: "UX review", "usability review", "heuristic evaluation", "accessibility audit", "is this usable". argument-hint: "[feature / screen / URL / mockup]" effort: high --- # UX review ## What I'll do Evaluate a design or implementation for usability, accessibility, and user experience quality using established heuristic frameworks. ## Inputs
development
--- name: user-flow description: Map user journeys through a feature or product, identifying key paths, decision points, friction, error states, and edge cases. Triggers: "user flow", "user journey", "flow diagram", "happy path", "user path". argument-hint: "[feature / user goal]" effort: medium --- # User flow ## What I'll do Map the complete user journey for a feature — from entry point through completion — including happy paths, error states, edge cases, and decision points. > **user-flow
development
Use this skill to produce complete UI/UX design artifacts from a specification document or panel analysis. Triggers include: "design the UI for this spec", "create wireframes", "design this panel", "UX design from spec", "generate component specs", "design tokens", "create the UI design for", "design system for", "wireframe this feature", "design a UI", "create a design system", "design this component", "design the layout", "create a style guide", "design a screen", "UI/UX review", "typography system", "color system", "spacing system", "design this feature", "design the dashboard", "design the onboarding", "create a component library", "design review", "audit the design", "improve the UI", "redesign this", "design system documentation", "create design guidelines", "responsive design", "mobile design", "dark mode design", "design the brand", or any time a spec/panel analysis document needs to be transformed into actionable UI/UX deliverables before implementation. Also triggers for standalone design system creation, component design, design reviews, dark mode/responsive variants, and developer handoff — even before code is involved. Orchestrates a multi-agent design team (UX Lead, UI Designer, Component Architect, Accessibility Reviewer, Design System Engineer, Design Reviewer) in parallel waves. Outputs feed directly into spec-to-impl's FE agent and figma-to-code.