↑ ↓ para mover · Enter para abrir · Esc para fecharToque em um resultado

Arquitetura
skill · classe design · opus

rest-api-architect

Projeta o contrato HTTP: endpoints, DTOs, mapa de erros, paginação, idempotência e versionamento.

skill · designopusescreve 30-rest.mdsó dentro de docs/use-cases/UC-*/**
§ o arquivo que ela escreve

30-rest.md, bloco a bloco

Um parcial real: UC-001-create-customer, no bank-app, em trechos. Passe o mouse ou clique num bloco para ver o que a skill fixa ali. As seções do arquivo também são chips: clique em um para ir até ela.

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.

… cortado aqui, o resto está no arquivo

## 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—

… cortado aqui, o resto está no arquivo

## 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.

… cortado aqui, o resto está no arquivo

## 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)

… cortado aqui, o resto está no arquivo

## 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.

Abrir o arquivo completo no GitHub →
§ comportamento

O que lê, escreve e recusa

O contrato da própria skill, do SKILL.md, no texto original.

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.

Fonte: SKILL.md § Contract, no nerviz e4cae27.

§ guard e hook

O que o guard faz com ela

design é a classe da skill. O ArchHook abre uma fase quando a skill é chamada e confere cada escrita contra o território da classe. O texto à direita é o do próprio hook.

write_allow da classe design

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**: migrations e classes são do executor
  • docker-compose.yml: arquivo de outra classe
  • caso de uso já aprovado: as specs ficam congeladas

A recusa sai com código 2 e a mensagem volta para o modelo.

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
§ orquestração

Onde ela fica no trilho

Cada cartão é um passo do /new-feature; a seta leva o parcial que um passo entrega ao seguinte. Passos tracejados só rodam quando o caso de uso precisa.

/new-feature-implement Quem lê o parcial depois

Depois da aprovação, o /new-feature-implement faz o executor construir o código. A spec é consolidada por referência e vale onde ela e um parcial divergem; o parcial é lido, por seção, para o detalhe.

30-rest.mddocs/use-cases/UC-NNN-…/
lido por seção, para o detalhe
java-spring-boot-developeragent · executor
escreve o código
src/**desenha um caso de uso numa spec, não escreve código
← voltar às cadeias