| name | api-contract |
| description | Define a schema-first API contract — standardized error envelope (RFC 9457), pagination, status codes, consistent JSON shapes. Use when establishing API conventions, before multiple teams consume an API, or when error responses are inconsistent. Not for choosing the protocol or modeling resources (use api-design) or for runtime input parsing at the boundary (use data-validation). |
| license | MIT |
API Contract (Schema-First)
Purpose
Establish one consistent contract — error envelope, pagination, status codes, JSON shape — so clients can be written against a stable, extensible API and breaking changes are caught before they ship.
Universal — RFC 9457 problem+json, cursor-vs-offset pagination, status-code semantics, and "object-as-root" are HTTP/spec-level conventions that apply to any backend.
Procedure
-
Standardize the error envelope (RFC 9457 application/problem+json)
{ type, title, status, detail, instance } + domain extensions
- One envelope for ALL errors — clients parse it once
- Never return bare strings or inconsistent shapes per endpoint
-
Always return a top-level JSON object (never a bare array)
{ "items": [...], "nextCursor": "..." } not [...]
- Lets the contract add fields (pagination meta, warnings) without breaking clients
-
Pick a pagination strategy and apply it everywhere
- Cursor-based (preferred) — stable under concurrent writes, scales
- Offset-based — simple, acceptable for small/static datasets
- Don't mix strategies across endpoints
-
Use status codes by their HTTP semantics
200/201/204 success; 400 validation; 401 unauthenticated; 403 unauthorized; 404 not found; 409 conflict; 422 semantic validation; 429 rate-limited; 5xx server
4xx = client must change request; 5xx = client may retry
429 must include Retry-After (seconds or HTTP-date) — without it clients retry blindly and amplify the storm; rate-limit metadata in RateLimit-* headers (IETF draft) helps well-behaved clients self-throttle
4b. Idempotency header for non-idempotent endpoints
- Accept an
Idempotency-Key: <uuid> header on POSTs that create/charge — store key→result so a retried request returns the original result, not a duplicate effect (see resilience-patterns)
- Document it in the contract: clients can safely retry POST without double-effects
4c. Optimistic concurrency via ETag / If-Match
- Read endpoint returns
ETag: "<version>"; write endpoint requires If-Match: "<version>" and returns 412 Precondition Failed on mismatch
- Prevents lost updates without coordination — the client sends back the version it read, the server rejects if anyone else wrote in between
-
Schema-first: generate, don't hand-write
- Define OpenAPI 3.1 schema (or generate from typed code) as source of truth
- Generate client types + server validation from the schema
- Lint the schema in CI (Spectral / Zally)
-
Validate (validation loop)
- Run the schema linter; if rule violations, fix and re-run until clean
- Diff the new schema against the previous version; if a field was removed/renamed/retyped → it's a breaking change → bump version or make additive
Anti-patterns
| ❌ Anti-pattern | ✅ Correct |
|---|
| Different error shape per endpoint | One RFC 9457 envelope everywhere |
Bare array response ([...]) | { items: [...], nextCursor } object |
200 OK with { error: "..." } in body | Correct status code (400/409/etc.) |
| Offset pagination on a high-write table | Cursor-based pagination |
| Hand-maintained client types drifting from server | Generate both from one OpenAPI schema |
429 with no Retry-After (clients retry blindly) | Retry-After (+ RateLimit-* headers) on every 429 |
| POST that creates a charge with no idempotency support | Accept Idempotency-Key header; store key→result |
| Last-write-wins on concurrent updates (silent loss) | ETag on read + If-Match on write → 412 on mismatch |
Completion Criteria
Output
- OpenAPI 3.1 schema: source of truth, linted in CI
- Generated artifacts: client types + server validation
- Contract doc: error envelope + pagination + status-code policy
- Commit format:
feat(api): add <endpoint> to contract / fix(api): standardize error envelope
Implementation
TypeScript + NestJS (default)
@nestjs/swagger generates OpenAPI from decorators (code-first)
- Error envelope: global
ExceptionFilter mapping to RFC 9457
- Pagination: cursor helper in a shared interceptor
- CI lint:
spectral lint openapi.json
Other stacks
- Python / FastAPI: OpenAPI auto-generated; RFC 9457 via custom exception handlers
- Go:
swaggo/swag or hand-written OpenAPI; problem+json middleware
- Universal: RFC 9457, cursor pagination, status-code semantics are spec-level
Related skills
api-design — choose protocol/resources first, then formalize the contract here
data-validation — the contract's request schema drives boundary validation
Reference
- Key insight encoded: Standardize one error envelope (
application/problem+json, RFC 9457) and always return a top-level JSON object (never a bare array) so the contract can extend without breaking clients.