REST Best Practices
Versioning, pagination, filtering, idempotency, rate limiting
Production REST APIs must address operational concerns beyond resource design: versioning to evolve contracts without breaking clients, pagination to handle large collections, idempotency to handle network retries safely, and rate limiting to protect backend capacity. These practices prevent the most common sources of API-related incidents — breaking changes pushed without warning, unbounded collection queries that OOM the database, duplicate POST side-effects from client retries, and traffic spikes that cascade to downstream services.
Key Points
- API versioning strategies: URI path (/v1/users — simple, cache-friendly), query parameter (?version=1 — messy), custom header (X-API-Version: 1 — clean but less visible), Accept header (application/vnd.myapp.v1+json — RESTful but complex client setup).
- URI versioning is the most widely adopted; maintain N-1 versions simultaneously; deprecate with Sunset header (RFC 8594) and a 12-month notice window minimum.
- Pagination: offset-based (?page=3&per_page=25 — simple but inconsistent under concurrent writes), cursor-based (?after=cursor_token — stable, recommended for feeds and large datasets), keyset-based (WHERE id > last_id — efficient, uses index).
- Idempotency keys: client generates a UUID, sends in Idempotency-Key header with POST; server stores (key → response) in Redis with TTL; on re-submission, returns the stored response — prevents duplicate charges.
- Rate limiting response: 429 Too Many Requests with Retry-After header (seconds until reset) and X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers for client transparency.
- Filtering and sorting: use query parameters (?status=active&sort=-created_at where "-" prefix means descending); document allowed filter fields and index them in the database.
- Field selection (sparse fieldsets): ?fields=id,name,email reduces payload size; critical for mobile APIs where bandwidth is expensive — reduces response size by 50–80% for partial reads.
- CORS: set Access-Control-Allow-Origin explicitly (never * in authenticated APIs); preflight OPTIONS cache with Access-Control-Max-Age: 86400 to avoid a preflight on every cross-origin request.
Real-World Example
Stripe uses idempotency keys for all mutating operations — their checkout sessions can be safely retried after network failures without double-charging. Twitter's v1.1 API used cursor-based pagination for timeline feeds because offset pagination caused massive inconsistency when tweets were deleted during page traversal.