API Design
Purpose
Pick the right protocol for each API surface by traffic characteristics — not by fashion — and model resources, versioning, and async operations so the contract can evolve without breaking clients.
Universal — protocol selection criteria (latency, client shaping needs, internal vs external), resource modeling, and versioning strategy apply to any language; only the framework's routing/serialization differs.
Procedure
Choose protocol by traffic shape
- REST — default for CRUD, public APIs, broad client compatibility, cacheable via HTTP
- gRPC — internal service-to-service, sub-10ms latency mesh, streaming, strict contracts
- GraphQL — clients need field-level shaping / aggregate many resources; accept the schema-design + N+1 discipline cost (requires a per-request batching/caching layer)
- Decide by concrete thresholds (latency budget, payload variability), not "it depends"
Model resources (REST)
- Nouns not verbs (
/orders/{id}not/getOrder) - Nest only one level deep (
/orders/{id}/items, not 4 levels) - Use sub-resources for relationships, query params for filtering
- Nouns not verbs (
Design versioning before v1 ships
- URL versioning (
/v1/) — simplest, most visible - Header versioning — cleaner URLs, less discoverable
- Commit to one; document the deprecation policy (sunset header + timeline)
- URL versioning (
Async / long-running operations
- Return
202 Accepted+ a status resource (/operations/{id}) for jobs > a few seconds - Don't hold the request open; pair with
background-jobsskill
- Return
Validate the design
- Walk the top 3 client use cases through the API — count round-trips
- If a common screen needs 5+ calls → consider GraphQL or a BFF aggregate endpoint
Anti-patterns
| ❌ Anti-pattern | ✅ Correct |
|---|---|
Verbs in paths (/createUser) |
Nouns + HTTP methods (POST /users) |
| GraphQL "because it's modern" for a simple CRUD API | REST default; GraphQL only when field-shaping is a real need |
| Breaking change shipped without a version bump | New version OR additive-only change (see api-contract) |
Deep nesting (/a/{}/b/{}/c/{}) |
Max one level; use query params / sub-resource links |
Completion Criteria
- Protocol choice justified by a concrete traffic characteristic (documented)
- Versioning strategy decided and documented before v1
- Top 3 client use cases verified for round-trip count
- Long-running ops return 202 + status resource
Output
- API design doc / ADR:
docs/adr/ADR-NNN-api-<surface>.md— protocol choice + rationale + versioning policy - Route definitions: with resource modeling applied
- Commit format:
feat(api): design <surface> endpoints [protocol: REST|gRPC|GraphQL]
Implementation
TypeScript + NestJS (default)
- REST: NestJS controllers +
@nestjs/swaggerfor OpenAPI generation - GraphQL:
@nestjs/graphql(code-first) + DataLoader for N+1 - gRPC:
@nestjs/microservicesgRPC transport +.protocontracts - Versioning: NestJS
enableVersioning({ type: VersioningType.URI })
Other stacks
- Python / FastAPI: native OpenAPI; Strawberry/Ariadne for GraphQL; grpcio for gRPC
- Go: chi/echo for REST; gqlgen for GraphQL; google.golang.org/grpc
- Universal: resource modeling, versioning, and 202-async patterns are protocol-level, not framework-level
Related skills
api-contract— once protocol is chosen, define the schema-first contract thereschema-design— resource modeling aligns with DB schemaauthorization— per-resource access control
Reference
- Key insight encoded: Pick protocol by traffic shape — REST default for CRUD, gRPC for sub-10ms internal mesh, GraphQL only when clients need field-level shaping (and you accept the schema-design discipline).