api-design-patterns
REST and GraphQL API design patterns.
Mandatory Rules (Blocking — violating any is an error)
- Every endpoint MUST return a consistent error envelope:
{ "error": { "code": "...", "message": "...", "details": [...] } }
- Never expose internal IDs in URLs without validation — all path params must be validated (UUID format, existence check)
- Never return 200 for errors — use proper 4xx/5xx status codes
- Every collection endpoint MUST support pagination — no unbounded lists
- Every mutating endpoint MUST be idempotent or document why not — include Idempotency-Key header support for POST
- Never break backwards compatibility — additive changes only without version bump
- Every endpoint MUST have rate limiting — return 429 with Retry-After header
- No sensitive data in URLs — tokens, passwords, PII go in headers or body, never query params
- All responses MUST include request-id header — for tracing and debugging
- OpenAPI spec MUST exist — no endpoint without a spec entry
Forbidden Patterns (Anti-patterns to reject on sight)
GET /getUsers — verb in resource name
POST /users/delete — using POST for deletion
200 OK with { "success": false } — lying status codes
- Nested resources deeper than 2 levels:
/a/{id}/b/{id}/c/{id}/d
- Mixed casing:
/userProfiles and /user-profiles in same API
- Returning full objects on DELETE (use 204 No Content)
- Pagination without total count or next-page cursor
- Error responses without machine-readable error codes
Decision Matrix — When to Use What
| Scenario |
Use |
Not |
| Need items 1-100 of 10K |
Cursor pagination |
Offset pagination |
| Sub-resource of a parent |
Nested URL /users/{id}/orders |
Flat URL /orders?user_id=X |
| Long-running operation |
202 Accepted + polling endpoint |
Synchronous wait |
| Bulk operations |
POST /bulk with array body |
Multiple individual calls |
| Search across types |
GET /search?q=... |
GET on each resource separately |
| File upload |
multipart/form-data |
Base64 in JSON body |
| Versioning |
URL prefix /v2/ |
Header-based (harder to test) |
| Auth |
Bearer token in Authorization header |
API key in query param |
how to use
when to apply
Reference these guidelines when:
- designing new API endpoints or resources
- choosing HTTP methods and status codes
- implementing error handling for APIs
- adding pagination, filtering, or sorting
- writing OpenAPI/Swagger specifications
- implementing rate limiting or auth
- reviewing API designs for consistency
rule categories by priority
| priority |
category |
impact |
| 1 |
resource naming |
critical |
| 2 |
HTTP methods |
critical |
| 3 |
status codes |
high |
| 4 |
error responses |
high |
| 5 |
pagination |
high |
| 6 |
versioning |
medium |
| 7 |
rate limiting |
medium |
| 8 |
idempotency |
medium |
quick reference
1. resource naming (critical)
- use nouns, not verbs:
/users not /getUsers
- use plural nouns:
/users, /orders, /products
- use lowercase with hyphens:
/user-profiles not /userProfiles
- nest for relationships:
/users/{id}/orders
- limit nesting to 2 levels max
- use query params for filtering:
/users?role=admin&status=active
- avoid trailing slashes; be consistent
2. HTTP methods (critical)
| Method |
Action |
Idempotent |
Safe |
Request Body |
| GET |
Read resource(s) |
Yes |
Yes |
No |
| POST |
Create resource |
No |
No |
Yes |
| PUT |
Replace resource entirely |
Yes |
No |
Yes |
| PATCH |
Partial update |
No |
No |
Yes |
| DELETE |
Remove resource |
Yes |
No |
Optional |
- GET must never modify state
- POST for actions that don't map to CRUD:
/orders/{id}/cancel
- PUT requires full resource representation
- PATCH sends only changed fields
3. status codes (high)
Success:
| Code |
Use |
| 200 |
Successful GET, PUT, PATCH, DELETE |
| 201 |
Successful POST (resource created); include Location header |
| 204 |
Successful DELETE or action with no response body |
Client errors:
| Code |
Use |
| 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 |
Valid syntax but unprocessable content |
| 429 |
Rate limit exceeded |
Server errors:
| Code |
Use |
| 500 |
Unexpected server error |
| 502 |
Bad gateway (upstream failure) |
| 503 |
Service unavailable (maintenance, overload) |
4. error responses (high)
Use a consistent error format across all endpoints:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable description",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
}
],
"request_id": "req_abc123"
}
}
- always include a machine-readable error code
- always include a human-readable message
- include field-level details for validation errors
- include request_id for traceability
- never expose stack traces or internal details in production
5. pagination (high)
Prefer cursor-based pagination for large or frequently changing datasets:
{
"data": [],
"pagination": {
"next_cursor": "eyJpZCI6MTAwfQ==",
"has_more": true,
"limit": 25
}
}
Offset-based acceptable for small, static datasets:
GET /users?offset=20&limit=10
Rules:
- default page size: 25; max: 100
- always return pagination metadata
- include total count only when cheap to compute
- return empty array (not null) for no results
6. versioning (medium)
Preferred: URL path versioning: /api/v1/users
- simple, explicit, cacheable
- increment major version only for breaking changes
Alternative: Header versioning: Accept: application/vnd.api+json;version=2
Rules:
- support at most 2 concurrent versions
- deprecation notice 6+ months before removal
- document breaking changes in changelog
- never break existing clients silently
7. rate limiting (medium)
- return rate limit headers on every response:
X-RateLimit-Limit: max requests per window
X-RateLimit-Remaining: remaining in current window
X-RateLimit-Reset: UTC epoch seconds when window resets
- return 429 with
Retry-After header when exceeded
- rate limit by API key, not just IP
- different limits for different endpoint tiers
8. idempotency (medium)
- GET, PUT, DELETE are naturally idempotent
- make POST idempotent with idempotency keys:
Idempotency-Key: <uuid>
- store idempotency results for replay (24h minimum)
- return cached result for duplicate idempotency keys
- critical for payment and financial endpoints
common fixes
| problem |
fix |
verb in URL (/getUser) |
rename to noun (/users/{id}) |
| 200 for errors |
use appropriate 4xx/5xx status codes |
| inconsistent error format |
adopt standard error envelope |
| no pagination |
add cursor-based pagination |
| breaking change without version bump |
increment API version |
| missing rate limiting |
add rate limiter middleware |
1---2name: api-design-patterns3description: REST and GraphQL API design patterns. Resource naming, HTTP methods, status codes, error formats, pagination, versioning, and rate limiting. Use when designing APIs, reviewing endpoints, or writing OpenAPI specs.4---56# api-design-patterns78REST and GraphQL API design patterns.910## Mandatory Rules (Blocking — violating any is an error)11121. **Every endpoint MUST return a consistent error envelope**: `{ "error": { "code": "...", "message": "...", "details": [...] } }`132. **Never expose internal IDs in URLs without validation** — all path params must be validated (UUID format, existence check)143. **Never return 200 for errors** — use proper 4xx/5xx status codes154. **Every collection endpoint MUST support pagination** — no unbounded lists165. **Every mutating endpoint MUST be idempotent or document why not** — include Idempotency-Key header support for POST176. **Never break backwards compatibility** — additive changes only without version bump187. **Every endpoint MUST have rate limiting** — return 429 with Retry-After header198. **No sensitive data in URLs** — tokens, passwords, PII go in headers or body, never query params209. **All responses MUST include request-id header** — for tracing and debugging2110. **OpenAPI spec MUST exist** — no endpoint without a spec entry2223## Forbidden Patterns (Anti-patterns to reject on sight)2425- `GET /getUsers` — verb in resource name26- `POST /users/delete` — using POST for deletion27- `200 OK` with `{ "success": false }` — lying status codes28- Nested resources deeper than 2 levels: `/a/{id}/b/{id}/c/{id}/d`29- Mixed casing: `/userProfiles` and `/user-profiles` in same API30- Returning full objects on DELETE (use 204 No Content)31- Pagination without total count or next-page cursor32- Error responses without machine-readable error codes3334## Decision Matrix — When to Use What3536| Scenario | Use | Not |37|----------|-----|-----|38| Need items 1-100 of 10K | Cursor pagination | Offset pagination |39| Sub-resource of a parent | Nested URL `/users/{id}/orders` | Flat URL `/orders?user_id=X` |40| Long-running operation | 202 Accepted + polling endpoint | Synchronous wait |41| Bulk operations | POST /bulk with array body | Multiple individual calls |42| Search across types | GET /search?q=... | GET on each resource separately |43| File upload | multipart/form-data | Base64 in JSON body |44| Versioning | URL prefix `/v2/` | Header-based (harder to test) |45| Auth | Bearer token in Authorization header | API key in query param |4647## how to use4849- `/api-design-patterns`50 Apply these API conventions to all endpoint design in this conversation.5152- `/api-design-patterns <endpoint>`53 Review the endpoint against rules below and suggest improvements.5455## when to apply5657Reference these guidelines when:58- designing new API endpoints or resources59- choosing HTTP methods and status codes60- implementing error handling for APIs61- adding pagination, filtering, or sorting62- writing OpenAPI/Swagger specifications63- implementing rate limiting or auth64- reviewing API designs for consistency6566## rule categories by priority6768| priority | category | impact |69|----------|----------|--------|70| 1 | resource naming | critical |71| 2 | HTTP methods | critical |72| 3 | status codes | high |73| 4 | error responses | high |74| 5 | pagination | high |75| 6 | versioning | medium |76| 7 | rate limiting | medium |77| 8 | idempotency | medium |7879## quick reference8081### 1. resource naming (critical)8283- use nouns, not verbs: `/users` not `/getUsers`84- use plural nouns: `/users`, `/orders`, `/products`85- use lowercase with hyphens: `/user-profiles` not `/userProfiles`86- nest for relationships: `/users/{id}/orders`87- limit nesting to 2 levels max88- use query params for filtering: `/users?role=admin&status=active`89- avoid trailing slashes; be consistent9091### 2. HTTP methods (critical)9293| Method | Action | Idempotent | Safe | Request Body |94|--------|--------|------------|------|-------------|95| GET | Read resource(s) | Yes | Yes | No |96| POST | Create resource | No | No | Yes |97| PUT | Replace resource entirely | Yes | No | Yes |98| PATCH | Partial update | No | No | Yes |99| DELETE | Remove resource | Yes | No | Optional |100101- GET must never modify state102- POST for actions that don't map to CRUD: `/orders/{id}/cancel`103- PUT requires full resource representation104- PATCH sends only changed fields105106### 3. status codes (high)107108**Success**:109| Code | Use |110|------|-----|111| 200 | Successful GET, PUT, PATCH, DELETE |112| 201 | Successful POST (resource created); include Location header |113| 204 | Successful DELETE or action with no response body |114115**Client errors**:116| Code | Use |117|------|-----|118| 400 | Malformed request, validation failure |119| 401 | Missing or invalid authentication |120| 403 | Authenticated but not authorized |121| 404 | Resource not found |122| 409 | Conflict (duplicate, state conflict) |123| 422 | Valid syntax but unprocessable content |124| 429 | Rate limit exceeded |125126**Server errors**:127| Code | Use |128|------|-----|129| 500 | Unexpected server error |130| 502 | Bad gateway (upstream failure) |131| 503 | Service unavailable (maintenance, overload) |132133### 4. error responses (high)134135Use a consistent error format across all endpoints:136137```json138{139 "error": {140 "code": "VALIDATION_ERROR",141 "message": "Human-readable description",142 "details": [143 {144 "field": "email",145 "message": "Invalid email format",146 "code": "INVALID_FORMAT"147 }148 ],149 "request_id": "req_abc123"150 }151}152```153154- always include a machine-readable error code155- always include a human-readable message156- include field-level details for validation errors157- include request_id for traceability158- never expose stack traces or internal details in production159160### 5. pagination (high)161162**Prefer cursor-based pagination** for large or frequently changing datasets:163```json164{165 "data": [],166 "pagination": {167 "next_cursor": "eyJpZCI6MTAwfQ==",168 "has_more": true,169 "limit": 25170 }171}172```173174**Offset-based** acceptable for small, static datasets:175```176GET /users?offset=20&limit=10177```178179Rules:180- default page size: 25; max: 100181- always return pagination metadata182- include total count only when cheap to compute183- return empty array (not null) for no results184185### 6. versioning (medium)186187**Preferred**: URL path versioning: `/api/v1/users`188- simple, explicit, cacheable189- increment major version only for breaking changes190191**Alternative**: Header versioning: `Accept: application/vnd.api+json;version=2`192193Rules:194- support at most 2 concurrent versions195- deprecation notice 6+ months before removal196- document breaking changes in changelog197- never break existing clients silently198199### 7. rate limiting (medium)200201- return rate limit headers on every response:202 - `X-RateLimit-Limit`: max requests per window203 - `X-RateLimit-Remaining`: remaining in current window204 - `X-RateLimit-Reset`: UTC epoch seconds when window resets205- return 429 with `Retry-After` header when exceeded206- rate limit by API key, not just IP207- different limits for different endpoint tiers208209### 8. idempotency (medium)210211- GET, PUT, DELETE are naturally idempotent212- make POST idempotent with idempotency keys: `Idempotency-Key: <uuid>`213- store idempotency results for replay (24h minimum)214- return cached result for duplicate idempotency keys215- critical for payment and financial endpoints216217## common fixes218219| problem | fix |220|---------|-----|221| verb in URL (`/getUser`) | rename to noun (`/users/{id}`) |222| 200 for errors | use appropriate 4xx/5xx status codes |223| inconsistent error format | adopt standard error envelope |224| no pagination | add cursor-based pagination |225| breaking change without version bump | increment API version |226| missing rate limiting | add rate limiter middleware |