↑ ↓ to move · Enter to open · Esc to closeTap a result

Architecture
skill · class design · opus

rest-api-architect

Designs the HTTP contract: endpoints, DTOs, error map, pagination, idempotency and versioning.

skill · designopuswrites 30-rest.mdonly inside docs/use-cases/UC-*/**
§ the file it writes

30-rest.md, block by block

A real partial: UC-001-create-customer, in bank-app, excerpted. Hover or click a block to see what the skill fixes there. The sections of the file are also chips: click one to jump to it.

bank-app/docs/use-cases/UC-001-create-customer/30-rest.md

UC-001 · REST adapter

Partial of docs/use-cases/UC-001-create-customer/. Owner: rest-api-architect. Inherits the canonical names from 00-caso-de-uso.md and the ports from 10-dominio.md — doesn't reinvent them. Contains no code. Reference shapes in .claude/skills/rest-api-architect/templates/. Rules applied, never reproduced: @.claude/rules/api-rest.md.
FieldValue
Parent spec00-caso-de-uso.md
Domain partial10-dominio.md
Resourcecustomers
Version prefix/api/v1
Divergences from 10-dominio.mdnone
Divergences from 00-caso-de-uso.mdnone — "reference only" is honoured as a body carrying id alone, plus Location; api-rest.md § Success statuses requires a body on 201, and a reduced representation satisfies both

Paths below are relative to src/main/java/dev/nerviz/bankapp/infrastructure/rest/. No @RestController exists in the project yet: every class here is NEW.

## 1 · Endpoints

MethodPathoperationIdInbound portSuccess
POST/api/v1/customerscreateCustomerCreateCustomerUseCase.create201 + Location: /api/v1/customers/{customerId} (relative, built from the controller's path constant) + CustomerResponse

No GET: the parent spec has no read use case, and an endpoint with no port behind it is a guessed contract. Location still points at the resource's canonical URI; it answers 404 until a read case exists (## Out of scope of the parent spec).

POST /api/v1/customers requires Idempotency-Key: it creates a resource (api-rest.md § Idempotency). The business duplicate guard (unique security number) does not replace it — it answers the second call with 409, where the protocol requires the retried call to get the original 201 back.

Access: public (parent spec). The project has no Spring Security filter chain; the idempotency key's caller identity is the constant anonymous — see block 4.

… cut here, the rest is in the file

## 2 · DTOs

DTODirectionFieldsShape validation
dto/CreateCustomerRequestinputname (String), securityNumber (String), birthDate (LocalDate, ISO-8601 yyyy-MM-dd)@NotNull on each field — presence only
dto/CustomerResponseoutputid (UUID)—

Every field carries @Schema with description, example and explicit requiredMode = REQUIRED. securityNumber's @Schema documents pattern = "^[0-9]{11}$" for the client; the pattern is not enforced with @Pattern — see block 3.

Why presence only. Every format rule (blank or out-of-range name, 11 digits, date limits) is a domain invariant with its own errorCode (10-dominio.md § 2). A @NotBlank, @Size or @Pattern on the same field would fire first, as a violations 400 without errorCode, and make the domain's code unreachable through this endpoint. Kept to @NotNull, the domain answers each format error with a 400 carrying its errorCode.

Masking — re-derived over the DTO fields by @.claude/rules/logging.md § Masking candidates. Same result as 10-dominio.md § 1, no divergence:

DTOFieldMatchAnnotation
CreateCustomerRequestsecurityNumbername (National id)@MaskSensitiveData(maskedType = MaskedType.DOCUMENT)
CreateCustomerRequestbirthDatename (Birth)@MaskSensitiveData(maskedType = MaskedType.DATE)
CreateCustomerRequestnamenone — counter-list—
CustomerResponseidnone — counter-list—

… cut here, the rest is in the file

## 3 · Error map

SituationDomain exceptionStatuserrorCodeExtra fields
Security number already registeredSecurityNumberAlreadyRegisteredException (ConflictException)409SECURITY_NUMBER_ALREADY_REGISTEREDerrorCode
Customer not strictly over 18BusinessRuleViolationException422CUSTOMER_UNDERAGEerrorCode
Name blank after trimmingValidationException400CUSTOMER_NAME_REQUIREDerrorCode
Name outside 2 to 120 characters after trimmingValidationException400CUSTOMER_NAME_LENGTHerrorCode
Security number not exactly 11 digitsValidationException400SECURITY_NUMBER_INVALIDerrorCode
Birth date in the futureValidationException400BIRTH_DATE_IN_FUTUREerrorCode
Age over 130ValidationException400BIRTH_DATE_TOO_OLDerrorCode
Same key, different bodyConflictException409IDEMPOTENCY_KEY_REUSEDerrorCode
Same key, request still in progressConflictException409 + Retry-AfterIDEMPOTENCY_KEY_IN_PROGRESSerrorCode
Idempotency-Key missing or not a UUID— (MissingIdempotencyKeyException, structural)400noneviolations
name, securityNumber or birthDate missing— (bean validation, @NotNull)400noneviolations
Unreadable JSON, birthDate not yyyy-MM-dd, wrong type— (deserialization)400none—
Unmapped—500nonetraceId

Unreachable through this endpoint, listed so no test asserts them: CUSTOMER_ID_REQUIRED and REGISTERED_AT_REQUIRED (set by the use case, never by the client); CUSTOMER_NAME_REQUIRED, SECURITY_NUMBER_REQUIRED and BIRTH_DATE_REQUIRED for a missing field — intercepted by @NotNull. CUSTOMER_NAME_REQUIRED stays reachable for a blank or whitespace-only name.

… cut here, the rest is in the file

## 4 · Pagination, idempotency, and dependencies

Pagination — none: no collection endpoint.

Idempotency — @Idempotent + IdempotencyAspect, first Idempotency-Key endpoint of the project, so every piece is NEW:

HalfWhere it livesIn this partial
Header presence and formatidempotent/IdempotencyKeyInterceptor + idempotent/MissingIdempotencyKeyException, registered by idempotent/IdempotencyWebConfig for handlers annotated @Idempotentyes — 400 above
Annotation and aspectidempotent/Idempotent, idempotent/IdempotencyAspect, idempotent/IdempotencyRequestHasher, idempotent/IdempotencyResponseCodec, idempotent/AspectInvocationException (templates/IdempotencyAspect.java.example)yes
Claim (own transaction), then effect + stored response (one transaction)application/shared/IdempotentExecution and its portno — declared; shape in persistence-architect's templates
idempotency_keys table20-persistencia.mdno — handed off to persistence-architect

Caller identity stored with the key: the constant anonymous — the endpoint is public and the project has no principal to read. When a later case secures the endpoint, the identity becomes the principal's, in that case's impact section.

Schema requirements — read by persistence-architect in its first pass:

RequirementWhy
idempotency_keys table, shared — NEW, first in the project (route, caller, body hash, status, stored response, lease, TTL ≥ 24 h)POST /api/v1/customers requires Idempotency-Key (api-rest.md § Idempotency)
A retention for expired idempotency_keys rows — the body hash and stored response are kept ≥ 24 h, never foreverTTL in api-rest.md § Idempotency; the stored response holds only id, so no personal data — the request body is stored as a hash only
customers expected volume: not given; no collection endpoint, so pagination doesn't depend on it—

Dependencies to add — the executor applies them to pom.xml:

ArtifactWhyVersion
org.springframework.boot:spring-boot-starter-aspectjIdempotencyAspect (@Aspect, @Around) — Boot 4's name for the former spring-boot-starter-aopmanaged by the Spring Boot BOM (parent 4.1.1)

… cut here, the rest is in the file

## 5 · Contract test cases

CaseRequestAsserts
Happy creationvalid POST + key201, Location = /api/v1/customers/{id}, body id equals the id in Location, no other field
Underage — 18th birthday todaybirthDate = today − 18 years422 and errorCode = CUSTOMER_UNDERAGE
Duplicate security numbersecond POST, new key, same securityNumber409 and errorCode = SECURITY_NUMBER_ALREADY_REGISTERED
Security number with 10 digits / with punctuation"1234567890" / "123.456.789-01"400 and errorCode = SECURITY_NUMBER_INVALID
Blank namename = " "400 and errorCode = CUSTOMER_NAME_REQUIRED
Name of 1 / 121 characters400 and errorCode = CUSTOMER_NAME_LENGTH
Future birth datebirthDate = tomorrow400 and errorCode = BIRTH_DATE_IN_FUTURE
Age 131birthDate = today − 131 years400 and errorCode = BIRTH_DATE_TOO_OLD
Missing fieldPOST without securityNumber400, violations[0].field = securityNumber, errorCode absent
Unparseable datebirthDate = "17/05/1990"400, no errorCode
Missing keyPOST without Idempotency-Key400, violations, no errorCode
Idempotent replaytwo identical POSTs, same keyone customer created; both 201 with identical Location and body
Reused keysame key, different body409 and errorCode = IDEMPOTENCY_KEY_REUSED
Unmapped failureuse case throws an unexpected exception500, traceId present, generic detail
No leakany error abovedetail contains neither the security number nor the birth date

The clock is fixed in the test so "today" is deterministic. Body-shape fixture: templates/error-responses.json.example.

## 6 · Personal data

none — the only response body is CustomerResponse { id }, and an aggregate's id is on the counter-list of @.claude/rules/logging.md § Masking candidates. personal-data.md § How to verify grep 1 over the response DTOs designed here: no hit. Security number and birth date enter in the request and never leave in a response or an error detail.

## Design patterns

none — no force in the spec, no symptom on disk: one resource, one operation, no @RestController in src/ to repeat. The idempotency aspect is api-rest.md's mandated mechanism, not a pattern chosen here.

## Impact on approved use cases

none — UC-001 is the first case.

Open the full file on GitHub →
§ behavior

What it reads, writes and refuses

The skill's own contract, from SKILL.md, in the original wording.

Class

design — the territory is skill_classes.design in @.claude/schemas/extensions.json and ArchHook.java guard enforces it. Writes inside the use case folder and nothing else; a controller or DTO written under src/ is refused with exit 2.

Reads

docs/use-cases/UC-NNN-<slug>/00-use-case.md and 10-domain.md (mandatory — stops without the second), @.claude/rules/api-rest.md, @.claude/rules/architecture-ddd.md (Adapters and Composition sections), @.claude/rules/naming.md, @.claude/rules/error-handling.md, @.claude/rules/code-quality.md, @.claude/rules/logging.md (masking mechanism for DTO fields 10-domain.md flagged sensitive), @.claude/rules/observability.md § Metrics (the bound on the per-version tag), the 30-rest.md that wrote the versioning setup when an earlier case did, @.claude/rules/inbound-admission.md and the partial that wrote the admission setup when the case has an Admission row, the root CLAUDE.md's **Transport: paragraph (trusted proxies, for a public endpoint's key), and the active blueprint's packages.map. Also reads 20-persistence.md when it exists (a resumed or hand-run case) — the table's expected volume decides offset or cursor. In /new-feature this skill runs first, so the volume comes from its own interview ("does the collection grow without bound") and is written in block 4, where persistence-architect reads it instead of asking again.

Writes

docs/use-cases/UC-NNN-<slug>/30-rest.md. Only that file.

Writes no Java code

The controller, the DTOs, the mapper, the interceptor, the ApiExceptionHandler, and the contract tests come from the executor agent. It doesn't edit pom.xml nor .github/workflows/** — the openapi.json diff job belongs to project-bootstrap.

Doesn't decide

the use case boundary (00-use-case.md), the domain model (10-domain.md), the schema or the idempotency key table (20-persistence.md), nor the test strategy (40-tests.md). Doesn't touch .claude/rules/**.

Doesn't collide with `domain-modeling`

: that one declares the inbound port, this one says over which HTTP it's called. The port's signature belongs to the other one; if it needs to change, report the divergence instead of rewriting it.

Source: SKILL.md § Contract, at nerviz e4cae27.

§ guard and hook

What the guard does with it

design is the skill's class. ArchHook opens a phase when the skill is called and checks every write against the class's territory. The text on the right is the hook's own.

write_allow of class design

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**: migrations and classes are the executor's
  • docker-compose.yml: another class's file
  • a use case already approved: its specs are frozen

A refusal exits with code 2 and the message goes back to the model.

PreToolUse  Skill(rest-api-architect)
  audit call                       exit 0
  guard call                       exit 0
# a skill of another class replaces the phase
# write_allow: docs/use-cases/UC-*/**, docs/use-cases/BACKLOG.md
§ orchestration

Where it sits on the rail

Each card is a step of /new-feature; the arrow carries the partial one step hands to the next. Dashed steps run only when the use case needs them.

/new-feature-implement Who reads the partial next

After approval, /new-feature-implement has the executor build the code. The spec is consolidated by reference and wins where it and a partial disagree; the partial is read, by section, for detail.

30-rest.mddocs/use-cases/UC-NNN-…/
read by section, for detail
java-spring-boot-developeragent · executor
writes the code
src/**designs one use case into a spec, writes no code
← back to the chains