Need to... → Method Idempotent? Safe?
Get a resource or collection → GET Yes Yes
Create a new resource → POST No No
Full replace of a resource → PUT Yes No
Partial update of a resource → PATCH No* No
Remove a resource → DELETE Yes No
Check if resource exists → HEAD Yes Yes
List allowed methods → OPTIONS Yes Yes
* PATCH can be idempotent if using JSON Merge Patch
Status Code Decision Tree
Success?
├── Created something new? → 201 Created (Location header)
├── Accepted for async processing? → 202 Accepted (include status URL)
├── No body to return? → 204 No Content
└── Returning data? → 200 OK
Client error?
├── Malformed request syntax? → 400 Bad Request
├── No/invalid credentials? → 401 Unauthorized
├── Valid credentials but insufficient permissions? → 403 Forbidden
├── Resource doesn't exist? → 404 Not Found
├── Method not allowed on resource? → 405 Method Not Allowed
├── Conflict with current state? → 409 Conflict
├── Resource permanently gone? → 410 Gone
├── Validation failed? → 422 Unprocessable Entity
├── Too many requests? → 429 Too Many Requests (Retry-After header)
└── Precondition failed (etag mismatch)? → 412 Precondition Failed
Server error?
├── Unexpected failure? → 500 Internal Server Error
├── Upstream dependency failed? → 502 Bad Gateway
├── Temporarily overloaded? → 503 Service Unavailable (Retry-After)
└── Upstream timeout? → 504 Gateway Timeout
# Filtering
GET /orders?status=active&created_after=2024-01-01&total_min=100
# Sorting (prefix - for descending)
GET /orders?sort=-created_at,total
# Field selection (reduce payload)
GET /orders?fields=id,status,total,customer.name
# Search
GET /products?q=wireless+headphones
# Combined
GET /orders?status=active&sort=-created_at&fields=id,status,total&page=1&per_page=10
Phase 2: OpenAPI Specification
Generate OpenAPI 3.1 Spec
For each resource in your design, generate a complete spec:
Validation Order:
1. Content-Type header (reject non-JSON early)
2. Authentication (401 before wasting cycles)
3. Authorization (403 - does this user have access?)
4. Path parameters (404 - does the resource exist?)
5. Query parameters (400 - valid types/ranges?)
6. Request body schema (422 - valid structure?)
7. Business rules (422 - valid state transition?)
Error Handling — Standard Error Codes
Define a consistent error code enum for your API:
# Authentication & Authorization
AUTH_REQUIRED — No credentials provided
AUTH_INVALID — Invalid/expired credentials
AUTH_INSUFFICIENT — Valid credentials, wrong permissions
AUTH_RATE_LIMITED — Too many auth attempts
# Validation
VALIDATION_FAILED — Generic validation error (see details array)
INVALID_FORMAT — Field format wrong (email, UUID, etc.)
REQUIRED_FIELD — Required field missing
OUT_OF_RANGE — Value outside allowed range
INVALID_ENUM — Value not in allowed set
# Resource
NOT_FOUND — Resource doesn't exist
ALREADY_EXISTS — Duplicate (unique constraint)
CONFLICT — State conflict (e.g., already cancelled)
GONE — Resource permanently deleted
# Business Logic
INSUFFICIENT_FUNDS — Payment-related
QUOTA_EXCEEDED — Usage limit reached
FEATURE_DISABLED — Feature flag off
DEPENDENCY_FAILED — Upstream service error
# System
INTERNAL_ERROR — Unexpected server error
SERVICE_UNAVAILABLE — Temporarily down
TIMEOUT — Request took too long
Idempotency
For non-idempotent operations (POST), require an idempotency key:
Request:
POST /orders
Idempotency-Key: ord_req_abc123
Server behavior:
1. Check if Idempotency-Key was seen before
2. If yes → return cached response (same status, same body)
3. If no → process request, cache response for 24h
4. Key format: client-generated UUID or meaningful string
Rate Limiting
Standard headers to include:
X-RateLimit-Limit: 100 # Max requests per window
X-RateLimit-Remaining: 67 # Remaining in current window
X-RateLimit-Reset: 1706886400 # Unix timestamp when window resets
Retry-After: 30 # Seconds to wait (on 429)
endpoint: POST /orders
tests:
happy_path:
- Creates order with valid data → 201
- Returns created resource with ID
- Location header points to new resource
- Timestamps are set (created_at, updated_at)
validation:
- Missing required fields → 422 with field-level errors
- Invalid field types (string where int expected) → 422
- Empty body → 400
- Invalid Content-Type → 415
- Extra unknown fields → ignored or 422 (pick one, be consistent)
- Boundary values (min/max length, 0, negative, empty string vs null)
authentication:
- No token → 401
- Expired token → 401
- Invalid token → 401
- Valid token, wrong scope → 403
authorization:
- User accessing own resource → 200
- User accessing other's resource → 403 or 404 (security choice)
- Admin accessing any resource → 200
edge_cases:
- Duplicate creation (same idempotency key) → same 201 response
- Concurrent creation race condition → one wins, one gets 409
- Resource at max relationships → 422
- Unicode in text fields → handled correctly
- Very long strings → 422 with max length error
- SQL injection in params → no effect (parameterized queries)
- XSS in text fields → stored safely, escaped on output
performance:
- Response time < 200ms (p95)
- List endpoint with 10K records → paginated, < 500ms
- Bulk operation timeout handling
curl Testing Recipes
# === Setup ===
BASE="https://api.example.com/v1"
TOKEN="your_bearer_token"
alias api='curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"'
# === CRUD Lifecycle Test ===
# Create
ORDER=$(api -X POST "$BASE/orders" -d '{"customer_id":"cust_1","items":[{"product_id":"prod_1","qty":2}]}')
ORDER_ID=$(echo "$ORDER" | jq -r '.data.id')
echo "Created: $ORDER_ID"
# Read
api "$BASE/orders/$ORDER_ID" | jq .
# Update
api -X PATCH "$BASE/orders/$ORDER_ID" -d '{"notes":"Rush order"}' | jq .
# List with filters
api "$BASE/orders?status=draft&sort=-created_at&per_page=5" | jq .
# Action (state transition)
api -X POST "$BASE/orders/$ORDER_ID/confirm" | jq .
# Delete
curl -s -o /dev/null -w "%{http_code}" -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE/orders/$ORDER_ID"
# === Error Testing ===
# No auth
curl -s "$BASE/orders" | jq .error
# Invalid body
api -X POST "$BASE/orders" -d '{"invalid": true}' | jq .error
# Not found
api "$BASE/orders/nonexistent" | jq .error
# === Performance ===
# Timing breakdown
curl -s -o /dev/null -w "DNS:%{time_namelookup} TCP:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer} Total:%{time_total}\n" -H "Authorization: Bearer $TOKEN" "$BASE/orders"
# Quick load test (50 requests, 10 concurrent)
seq 50 | xargs -P10 -I{} curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" -H "Authorization: Bearer $TOKEN" "$BASE/orders"
Contract Testing
Validate your API hasn't broken backward compatibility:
# contract-tests.yaml
contract:
name: Order API Contract
version: 1.0.0
rules:
# These changes are SAFE (non-breaking)
safe:
- Adding new optional fields to responses
- Adding new endpoints
- Adding new optional query parameters
- Adding new enum values (if clients handle unknown)
- Widening a constraint (min: 5 → min: 1)
# These changes are BREAKING
breaking:
- Removing a response field
- Renaming a response field
- Changing a field type
- Adding a new required request field
- Removing an endpoint
- Narrowing a constraint (max: 100 → max: 50)
- Changing error response format
- Removing an enum value
# Verify after every change
checks:
- All existing fields still present in responses
- All existing field types unchanged
- All existing required fields still required (no more, no fewer)
- Default values unchanged
- Error format unchanged
Phase 5: Security
Security Checklist (audit every API)
authentication:
- [ ] All endpoints require auth (except /health, /docs, public webhooks)
- [ ] Tokens expire (short-lived access + long-lived refresh)
- [ ] Token rotation supported
- [ ] Failed auth returns 401 with no info leakage
- [ ] API keys are hashed in storage (never plain text)
authorization:
- [ ] Resource-level checks (user can only access their data)
- [ ] Endpoint-level checks (role-based access)
- [ ] No IDOR vulnerabilities (can't guess other users' resource IDs)
- [ ] Admin endpoints separately protected
- [ ] Webhook endpoints verify signatures
input_validation:
- [ ] All inputs validated server-side (never trust client)
- [ ] SQL injection prevented (parameterized queries only)
- [ ] NoSQL injection prevented
- [ ] Path traversal prevented
- [ ] Request size limited (body, headers, URL length)
- [ ] File upload types restricted and scanned
output_security:
- [ ] No sensitive data in responses (passwords, tokens, internal IDs)
- [ ] No stack traces in production errors
- [ ] Consistent error format (no info leakage in different error types)
- [ ] PII redacted in logs
transport:
- [ ] HTTPS only (HTTP redirects to HTTPS)
- [ ] HSTS header set
- [ ] TLS 1.2+ required
- [ ] CORS configured restrictively (specific origins, not *)
headers:
- [ ] X-Content-Type-Options: nosniff
- [ ] X-Frame-Options: DENY
- [ ] Content-Security-Policy set
- [ ] No Server version header
- [ ] Cache-Control: no-store for sensitive endpoints
CORS Configuration
# Restrictive (recommended)
cors:
origins:
- https://app.example.com
- https://admin.example.com
methods: [GET, POST, PUT, PATCH, DELETE]
headers: [Authorization, Content-Type, X-Request-ID]
credentials: true
max_age: 3600
# Common mistakes to avoid:
# ❌ Access-Control-Allow-Origin: * (with credentials)
# ❌ Reflecting Origin header without validation
# ❌ Allowing all methods/headers
Phase 6: Versioning & Deprecation
Versioning Strategy Decision
Strategy
Example
Pros
Cons
Use When
URL path
/v1/orders
Explicit, easy routing
URL pollution
Public APIs, multiple major versions
Header
API-Version: 2024-01
Clean URLs
Hidden, harder to test
Internal APIs
Query param
?version=2
Easy to test
Pollutes params
Quick prototypes
Date-based
2024-01-15
Clear timeline
Many versions
Stripe-style APIs
Recommended: URL path for major versions, header for minor variations.
Deprecation Playbook
Timeline:
1. T+0: Announce deprecation (docs, changelog, email)
2. T+0: Add Deprecation + Sunset headers to old endpoints
3. T+30d: Log warnings for old endpoint usage
4. T+60d: Email heavy users of old endpoint directly
5. T+90d: Return 299 warning header
6. T+180d: Shut down old endpoint (410 Gone)
Headers:
Deprecation: true
Sunset: Sat, 01 Jun 2025 00:00:00 GMT
Link: <https://api.example.com/v2/orders>; rel="successor-version"
Migration Guide Template
# Migrating from v1 to v2
## Breaking Changes
1. `user.name` split into `user.first_name` + `user.last_name`
2. Pagination changed from offset to cursor-based
3. Error format updated (see new schema)
## Step-by-Step Migration
1. Update your client SDK to v2 (`npm install @example/sdk@2`)
2. Update response parsing for split name fields
3. Replace `?page=N` with `?after=cursor` pagination
4. Update error handling for new error format
## Compatibility Mode
Set `X-Compat-Mode: v1` header to get v1-style responses from v2 endpoints.
Available until 2025-06-01.
Phase 7: Monitoring & Observability
Key Metrics Dashboard
availability:
- Uptime percentage (target: 99.9% = 8.7h downtime/year)
- Health check status (/health endpoint)
- Error rate (5xx / total requests)
performance:
- p50 latency (target: < 100ms)
- p95 latency (target: < 500ms)
- p99 latency (target: < 1000ms)
- Throughput (requests/second)
- Time to first byte (TTFB)
business:
- Requests per endpoint (usage patterns)
- Unique API consumers/day
- Error rate by endpoint
- Rate limit hits/day
- Authentication failures/day
infrastructure:
- Database query time (p95)
- Connection pool utilization
- Memory/CPU per instance
- Queue depth (async operations)