API Design
Build interfaces that are hard to misuse. Design from the consumer's perspective.
Progress Checklist
Step 0: Context Check
This changes everything about the design:
| Question |
Internal API |
Public API |
| Who consumes it? |
Your team, known services |
Unknown third parties |
| Breaking changes? |
Coordinate and deploy together |
Version, deprecate, migrate |
| Auth complexity? |
Service-to-service tokens |
OAuth, API keys, rate limiting |
| Documentation? |
Enough for the team |
Comprehensive, with examples |
| Backward compat? |
Just change it |
Required, additive only |
Don't apply public-API rigor to an internal endpoint you own both sides of.
Step 1: Contract First
Define the interface before writing implementation:
- Write an OpenAPI/Swagger spec, or
- Define TypeScript types for request/response, or
- Define the protobuf/gRPC schema
This forces you to think about the shape of data before getting lost in business logic.
Step 2: Consumer-Driven Design
Write the calling code first (or imagine writing it). Ask:
- Is this intuitive to call?
- Are there too many required parameters?
- Are defaults sane? (Do I have to pass 10 options to do the common thing?)
- Can I understand what this endpoint does from its name and parameters?
If calling the API is awkward, fix the API. Don't make consumers work around bad design.
Sane Defaults
Every parameter should have a sensible default where possible:
- Pagination: default page size (e.g., 20), max page size (e.g., 100)
- Sorting: default sort order that makes sense for the use case
- Filtering: no filter = return all (within pagination)
- Timeouts: reasonable defaults, don't require the caller to set them
Step 3: Validate at Boundaries
All input validation happens at the API layer. Not in services, not in repositories.
- Use schema validation (zod, FluentValidation, go-playground/validator)
- Reject unexpected fields explicitly
- Validate types, ranges, formats, and required fields
- Return clear validation error messages with field names
See ../_shared/security-checklist.md for security-specific validation patterns.
Step 4: Consistent Error Responses
Every error should follow the same format. Never leak internals.
See references/api-patterns.md for error response formats and examples.
Key rules:
- Same structure for all errors (even 500s)
- Include a machine-readable error code
- Include a human-readable message
- Never expose stack traces, SQL, file paths, or internal IDs in production
- Log the details server-side, return the summary to the client
Step 5: Standard Patterns
- Pagination on every list endpoint. No exceptions. Cursor-based or offset-based.
- Rate limiting on auth endpoints and any endpoint that triggers expensive operations
- Idempotency for operations that change state (use idempotency keys for payment, creation endpoints)
- PATCH for partial updates, PUT for full replacement (or just use PATCH)
See references/api-patterns.md for detailed patterns.
Step 6: Generate Types
Don't hand-write API types on the client side:
| Stack |
Tool |
| TypeScript |
openapi-typescript, orval, openapi-generator |
| .NET |
NSwag, Kiota |
| Go |
oapi-codegen |
The spec is the source of truth. Generated types stay in sync automatically.
1---2name: api-design3description: Designs APIs with contract-first, consumer-driven approach including validation at boundaries, consistent error responses, and security focus. Generates types from OpenAPI/Swagger. Use when designing an API, creating endpoints, API architecture, REST design, building an API, defining contracts, new service, or when starting API work.4---56# API Design78Build interfaces that are hard to misuse. Design from the consumer's perspective.910## Progress Checklist1112- [ ] Determine context (internal vs public)13- [ ] Define contract first (OpenAPI/types)14- [ ] Design from consumer perspective15- [ ] Validate at boundaries16- [ ] Consistent error responses17- [ ] Sane defaults18- [ ] Generate types from spec1920## Step 0: Context Check2122This changes everything about the design:2324| Question | Internal API | Public API |25|----------|-------------|-----------|26| Who consumes it? | Your team, known services | Unknown third parties |27| Breaking changes? | Coordinate and deploy together | Version, deprecate, migrate |28| Auth complexity? | Service-to-service tokens | OAuth, API keys, rate limiting |29| Documentation? | Enough for the team | Comprehensive, with examples |30| Backward compat? | Just change it | Required, additive only |3132Don't apply public-API rigor to an internal endpoint you own both sides of.3334## Step 1: Contract First3536Define the interface before writing implementation:3738- Write an OpenAPI/Swagger spec, or39- Define TypeScript types for request/response, or40- Define the protobuf/gRPC schema4142This forces you to think about the shape of data before getting lost in business logic.4344## Step 2: Consumer-Driven Design4546Write the calling code first (or imagine writing it). Ask:4748- Is this intuitive to call?49- Are there too many required parameters?50- Are defaults sane? (Do I have to pass 10 options to do the common thing?)51- Can I understand what this endpoint does from its name and parameters?5253If calling the API is awkward, fix the API. Don't make consumers work around bad design.5455### Sane Defaults5657Every parameter should have a sensible default where possible:58- Pagination: default page size (e.g., 20), max page size (e.g., 100)59- Sorting: default sort order that makes sense for the use case60- Filtering: no filter = return all (within pagination)61- Timeouts: reasonable defaults, don't require the caller to set them6263## Step 3: Validate at Boundaries6465All input validation happens at the API layer. Not in services, not in repositories.6667- Use schema validation (zod, FluentValidation, go-playground/validator)68- Reject unexpected fields explicitly69- Validate types, ranges, formats, and required fields70- Return clear validation error messages with field names7172See `../_shared/security-checklist.md` for security-specific validation patterns.7374## Step 4: Consistent Error Responses7576Every error should follow the same format. Never leak internals.7778See `references/api-patterns.md` for error response formats and examples.7980Key rules:81- Same structure for all errors (even 500s)82- Include a machine-readable error code83- Include a human-readable message84- Never expose stack traces, SQL, file paths, or internal IDs in production85- Log the details server-side, return the summary to the client8687## Step 5: Standard Patterns8889- **Pagination** on every list endpoint. No exceptions. Cursor-based or offset-based.90- **Rate limiting** on auth endpoints and any endpoint that triggers expensive operations91- **Idempotency** for operations that change state (use idempotency keys for payment, creation endpoints)92- **PATCH for partial updates**, PUT for full replacement (or just use PATCH)9394See `references/api-patterns.md` for detailed patterns.9596## Step 6: Generate Types9798Don't hand-write API types on the client side:99100| Stack | Tool |101|-------|------|102| TypeScript | openapi-typescript, orval, openapi-generator |103| .NET | NSwag, Kiota |104| Go | oapi-codegen |105106The spec is the source of truth. Generated types stay in sync automatically.