Do NOT use for: pure internal refactors with no wire-format change, or UI-only work.
-
Pin the contract surface. List every consumer-visible element you touch: METHOD /path, query params, request body fields, response body fields, status codes, headers. If you can't enumerate it, you can't review it.
-
Model resources as nouns, actions as verbs. Path = pluralized noun collection (/orders, /orders/{id}/items). Never put the action in the path (/getOrder, /createOrder are wrong). Map intent to verb:
GET read, no side effects, safe + idempotent.
POST create / non-idempotent action → returns 201 + Location header (or 202 for async).
PUT full replace, idempotent. PATCH partial update.
DELETE remove, idempotent → 204 (or 200 with body).
- Genuinely non-CRUD action → POST a sub-resource (
POST /orders/{id}/cancel), not a verb path.
-
Pick the correct status code — not a blanket 200.
201 created (with Location), 202 accepted-async, 204 success-no-body.
400 malformed syntax, 401 unauthenticated, 403 authenticated-but-forbidden, 404 not found, 409 conflict (e.g. duplicate, state clash), 422 well-formed but semantically invalid (validation), 429 rate-limited.
5xx only for server faults — never for client validation failures.
-
Standardize pagination/filter/sort across the surface. Pick ONE pagination style and apply it everywhere: cursor-based (?cursor=&limit=, opaque cursor, stable for live data — preferred) or offset (?page=&per_page=, simpler but drifts on inserts). Return pagination metadata consistently (e.g. { "data": [...], "next_cursor": "...", "has_more": true }). Filtering/sorting via explicit allow-listed params (?status=open&sort=-created_at); reject unknown params instead of silently ignoring.
-
Use a single error envelope for every error response. Same shape on every 4xx/5xx so clients parse once:
{ "error": { "code": "VALIDATION_FAILED", "message": "human readable", "details": [ { "field": "email", "issue": "invalid format" } ] } }
code = stable machine string (clients branch on this, never on message). Don't leak stack traces / internal SQL.
-
Require idempotency for unsafe writes. Non-idempotent POST that creates resources or moves money/state must accept an Idempotency-Key header; server stores key→result and replays the same response on retry. PUT/DELETE must be naturally idempotent (calling twice = same end state, no error on second DELETE of already-gone resource → 204 or 404, pick one and document it).
-
Version + run a breaking-change diff. Diff the new contract against the existing one. Backward-compatible (safe, no version bump): adding an optional field, adding a new endpoint, adding an enum value clients must tolerate, loosening validation. Breaking (needs new version or is forbidden): removing/renaming a field, changing a type, making an optional field required, tightening validation, changing status-code semantics, changing default behavior. Version via URL (/v2/...) or header — pick the project's existing convention. If a change is breaking, either make it additive instead, or ship under a new version and keep the old one working.
-
Validate input at the edge. Reject unknown/extra fields or define the policy explicitly. Enforce types, ranges, lengths, and required-ness before any business logic runs. Document every field: type, required?, default, constraints, example.
If a contract test / schema snapshot exists, run it and confirm the diff matches the intended (and documented) changes only.