↑ ↓ 移动 · Enter 打开 · Esc 关闭点按一个结果

架构
skill · design 类 · opus

rest-api-architect

设计 HTTP 契约:端点、DTO、错误映射、分页、幂等和版本。

skill · designopus写入 30-rest.md仅限 docs/use-cases/UC-*/**
§ 它写出的文件

30-rest.md,逐块说明

一份真实的分文件:bank-app 中的 UC-001-create-customer,为节选。把鼠标移到某一块上或点击它,可以看到 skill 在这里固定了什么。文件的各个章节也是标签:点击即可跳转。

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.

… 此处截断,其余见文件

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

… 此处截断,其余见文件

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

… 此处截断,其余见文件

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

… 此处截断,其余见文件

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

在 GitHub 上打开完整文件 →
§ 行为

它读什么、写什么、拒绝什么

skill 自己的契约,取自 SKILL.md,保留原文。

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.

来源:SKILL.md § Contract(nerviz e4cae27)。

§ guard 与 hook

guard 如何处理它

design 是这个 skill 的类。ArchHook 在 skill 被调用时开启一个阶段,并按该类的领地检查每次写入。右侧文字是 hook 自己的输出。

design 类的 write_allow

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**:迁移和类归执行器
  • docker-compose.yml:另一个类的文件
  • 已批准的用例:其 spec 已冻结

拒绝以退出码 2 结束,消息回到模型。

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
§ 编排

它在轨道上的位置

每张卡片是 /new-feature 的一步;箭头带着一步交给下一步的分文件。虚线的步骤仅在用例需要时运行。

/new-feature-implement 之后谁读这份分文件

批准之后,/new-feature-implement 让执行器构建代码。spec 按引用整合,与分文件不一致时以 spec 为准;分文件按章节读取,用于细节。

30-rest.mddocs/use-cases/UC-NNN-…/
按章节读取,用于细节
java-spring-boot-developeragent · executor
写出代码
src/**把一个用例设计成规格,不写代码
← 返回链路