An HTTP API is a contract other code builds against. A contract that reinvents HTTP semantics or ships without its documentation can only be corrected later with a breaking change, so the rules apply while designing, not in review afterwards.
The rules live in the references; each step names the file it needs. A project convention that contradicts a rule wins inside that project, but name the contradiction to the caller as a conflict instead of passing over it. A departure from a MUST rule carries its reason in the OpenAPI document.
1. Fix the per-API choices
Several rules ask for one choice per API. Deciding them per endpoint is how one API ends up with two casings. Look for recorded values in the OpenAPI document, CLAUDE.md or AGENTS.md, docs/adr/ and the architecture issue.
| Choice | Default when nothing is recorded |
|---|---|
| Field and query parameter casing | camelCase |
| Enum value casing | UPPER_SNAKE_CASE |
| Resource ID format | UUIDv7 |
| Pagination | cursor |
| Patch format | JSON Merge Patch |
| Major version | path prefix /v1 |
| OpenAPI document and Bruno collection location | existing ones, otherwise ask |
For each choice without a recorded value, propose the default to the caller and record the agreed value where the project keeps its conventions. Inside a run nobody can answer, such as implement-ticket, stop and report the missing choice instead.
Done when every choice in the table has a recorded value.
2. Model resources, methods and status codes
Read resources, methods and status codes.
- Resources are plural nouns in
kebab-case, nested at most two levels. A state transition such as a cancellation becomes a sub-resource, not a verb in the path. - The method follows its semantics:
GETwithout side effects,PUTreplaces fully,PATCHwith a declared patch media type,DELETEidempotent. - List every status code an operation can return, including
401,403,404,409,422and429where they apply. Codes nobody listed become500in the implementation.
Done when every operation has a path, a method and its complete list of status codes.
3. Define errors and payloads
Read errors and payloads.
- Every
4xxand5xxisapplication/problem+jsonwith a stabletypeURI; clients program againsttype, so each operation lists thetypevalues it can return. - Every field gets its type from the types table: timestamps in UTC with
Z, money as decimal string plus currency, integers above 2^53 as strings. - Responses are objects, never bare arrays; empty collections are
[].
Done when every error response names its problem type and every field has a type from the table.
4. Decide state and load behaviour
Read collections, concurrency, caching and idempotency when the change has a collection, a mutable resource, a POST with a business effect or an operation that runs longer than about two seconds.
- Collections: cursor pagination, capped
limit, allowlisted filters and a total sort order. - Mutable resources:
ETagonGET,If-Matchon writes,412and428. - Every response: explicit
Cache-Control,Varywhere the response varies. POSTwith a business effect:Idempotency-Key.- Long-running work:
202with a job resource.
Done when each of the five points is either specified for the affected operations or named as not applicable.
5. Secure the operations
Read security and data minimisation when the change touches authentication, CORS, rate limits, client-supplied URLs, webhooks or personal data.
Every operation, whether or not the file is read:
- names its authentication and the scopes or roles it needs, and authorises per resource instance, because a check per endpoint lets one customer read another's objects;
- validates the request against the schema before business logic, with
additionalProperties: falseand bounded strings, numbers and arrays; - carries no personal data or secrets in its path or query string.
Done when every operation names its auth and scopes and has bounded request schemas, and no path or query holds personal data.
6. Classify changes to existing operations
Skip this step for new operations. Otherwise read versioning, evolution and documentation.
Classify every change to an existing operation as non-breaking or breaking. A breaking change needs a new major version, and that decision belongs to the caller: stop and ask. Deprecated operations get Deprecation and Sunset headers.
Done when every change to an existing operation is classified, and every breaking one is decided by the caller.
7. Update the contract artifacts
Read contract artifacts.
In the same change as the design or code:
- the OpenAPI 3.1 document describes every added or changed operation completely and drops removed ones;
- Swagger UI serves that same document and is not publicly reachable in production;
- the Bruno collection has one request per operation, with assertions for status code, content type and the headers the rules require.
An endpoint whose OpenAPI entry or Bruno request lags behind is undocumented for every client and every reviewer, so the artifacts are part of the change, not a follow-up.
Done when every added or changed operation is in the OpenAPI document and has a Bruno request with assertions, and every removed operation is gone from both.
8. Check and report
Go through the checklist for the operations touched. When the change also configures a server, reverse proxy, compression, health endpoints or metrics, read transport and observability as well.
Report to the caller:
- the per-API choices and where they are recorded,
- conflicts between rules and project conventions, quoting both,
- departures from MUST rules with their reasons,
- checklist items marked not applicable, with one reason each.
Done when every checklist item is ticked or marked not applicable with a reason, and the report is given.