API Design Checklist
Reference for designing consistent, predictable, and maintainable backend APIs. Read when building new endpoints, reviewing API contracts, or standardizing error handling.
Table of Contents
- REST Route Naming
- Request and Response Schemas
- Status Code Conventions
- Pagination
- Filtering and Sorting
- Versioning
- Idempotency
- GraphQL Resolver Boundaries
- WebSocket Event Contracts
- API Compatibility Rules
REST Route Naming
- Use plural nouns for resource collections:
/users,/orders,/projects. - Use resource IDs in path for single resources:
/users/:id,/orders/:id. - Nest routes only one level deep:
/users/:id/ordersis fine./users/:id/orders/:orderId/items/:itemId→ flatten to/order-items/:id. - Use kebab-case for multi-word resources:
/project-members, not/projectMembers. - Use verbs only for non-CRUD actions:
POST /orders/:id/cancel,POST /users/:id/verify-email. - Keep URLs under 2048 characters. Move complex filters to request body on POST if needed.
Request and Response Schemas
Request Validation
- Validate at the boundary — before business logic executes.
- Use schema validation libraries (Zod, Joi, Pydantic, JSON Schema, class-validator).
- Reject unknown fields in strict APIs. Allow and ignore in tolerant APIs.
- Validate types, required fields, string lengths, numeric ranges, enum values, email/URL formats.
- Return all validation errors at once, not one at a time.
Response Shape
Standard success response:
{
"data": { ... },
"meta": { "page": 1, "total": 42 }
}Standard error response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable summary",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
}
}- Wrap single resources and lists in
datafield for consistency. - Include
metafor pagination, rate limit info, or request tracing. - Use machine-readable
codealongside human-readablemessagein errors.
Status Code Conventions
| Code | When to use |
|---|---|
200 |
Successful read, update, or action |
201 |
Resource created |
204 |
Successful delete or action with no body |
400 |
Malformed request, validation failure |
401 |
Missing or invalid authentication |
403 |
Authenticated but not authorized |
404 |
Resource not found |
409 |
Conflict (duplicate, state conflict) |
422 |
Use only if project convention already uses it |
429 |
Rate limited |
500 |
Unexpected server error |
- Never return
200with an error body. Use proper status codes. - Return
404for missing resources even if the user lacks permission (prevents enumeration).
Pagination
Offset-based (simple)
GET /users?page=2&limit=20Response meta:
{ "page": 2, "limit": 20, "total": 142 }- Default limit: 20. Max limit: 100.
- Return
totalcount when feasible. Skip on expensive count queries.
Cursor-based (scalable)
GET /messages?cursor=abc123&limit=50Response:
{ "data": [...], "meta": { "next_cursor": "def456", "has_more": true } }- Use cursor-based for real-time feeds, large datasets, or frequently-changing data.
- Cursors should be opaque strings (base64-encoded IDs or timestamps).
Filtering and Sorting
GET /orders?status=shipped&created_after=2024-01-01&sort=-created_at- Use query params for simple filters.
- Prefix sort fields with
-for descending. - Validate filter and sort fields against an allowlist. Reject unknown fields.
- For complex queries, accept a JSON body on a POST endpoint (e.g.,
POST /orders/search).
Versioning
- Prefer URL prefix versioning:
/v1/users,/v2/users. - Version only when making breaking changes.
- Breaking changes: removing fields, changing types, renaming fields, altering behavior.
- Non-breaking changes: adding optional fields, new endpoints, new enum values.
- Document deprecation timeline. Sunset old versions with warning headers.
Idempotency
- All GET, PUT, DELETE operations should be naturally idempotent.
- For POST operations that must not duplicate (payments, orders), require an
Idempotency-Keyheader. - Store idempotency key → response mapping server-side with TTL (24–72 hours).
- Return cached response for duplicate key within TTL.
- Use database unique constraints as a safety net for critical operations.
GraphQL Resolver Boundaries
- Resolvers handle data fetching and field mapping. Business logic belongs in services.
- Use DataLoader pattern to batch and cache database queries within a request (prevents N+1).
- Validate mutations with same rigor as REST input validation.
- Return typed error unions or error extensions — not generic string messages.
- Limit query depth and complexity to prevent abuse (use
graphql-depth-limitor equivalent).
WebSocket Event Contracts
{
"type": "message.created",
"payload": { "id": "msg_123", "text": "Hello", "sender_id": "usr_456" },
"timestamp": "2024-01-15T10:30:00Z"
}- Use
typefield withresource.actionnaming convention. - Include
timestampon all events. - Authenticate on connection handshake (token in query param or first message).
- Send periodic heartbeat/ping to detect dead connections.
- Define reconnection behavior and missed-message recovery strategy.
API Compatibility Rules
Safe (non-breaking) changes
- Adding new optional fields to responses
- Adding new endpoints
- Adding new optional query parameters
- Adding new enum values (if clients handle unknown values)
- Adding new headers
Breaking changes (require versioning)
- Removing or renaming response fields
- Changing field types
- Removing endpoints
- Changing URL structure
- Making optional parameters required
- Changing error response shape
- Changing authentication mechanism