Designing REST APIs that last: clarity, compatibility, and speed
Great APIs are boring—in the best way. They’re predictable, forgiving, and fast. They age well because their contract is clear. This guide distills patterns we apply to build REST APIs that survive multiple frontends and years of change.
Model resources, not endpoints
Think nouns: /users
, /orders
, /orders/{id}
. Keep actions implicit in HTTP methods. When you need verbs, use subresources: /orders/{id}/cancel
.
Representation tips:
- Use canonical resource names and stable IDs (UUIDs/snowflakes)
- Support sparse fieldsets (
?fields=id,name,status
) to reduce payloads - Embed small related objects when it saves roundtrips; link for large collections
Consistent responses
Return a top-level object with data
, optional meta
, and error
only when something fails. Document shapes with OpenAPI and generate clients to avoid drift.
{
"data": { "id": "o_123", "status": "pending" },
"meta": { "request_id": "req_abc" }
}
Errors clients can act on
Use standard HTTP codes. Include a machine-readable code
, a human message
, and fields
for validation.
{
"error": {
"code": "validation_failed",
"message": "Email is invalid",
"fields": { "email": "invalid_format" }
}
}
Pagination that scales
Prefer cursor pagination via ?cursor=...&limit=50
. Return next_cursor
in meta
. It’s cheap for the DB and simple for clients. For analytics, add search_after
patterns.
Cursor design:
- Cursor encodes sort key + tie‑breaker (e.g.,
created_at,id
) - Always return
next_cursor
; optionallyprev_cursor
for bidirectional - Keep stable ordering; avoid
limit * page
offsets at scale
Caching and conditional requests
Emit strong ETags and support If-None-Match
. For lists, cache by filter signature. Honor Cache-Control
with sensible max-age; invalidate precisely when data changes.
ETag strategy:
- Compute from version fields (updated_at, revision) not full payload hash
- For writes, return new ETag and honor
If-Match
to prevent lost updates
Versioning without pain
Design for additive changes: new fields, new endpoints. When breaking changes are unavoidable, use header-based or URL-based versions and long deprecation windows. Communicate.
Practical rules:
- Never reuse field semantics; add a new field instead
- Don’t remove fields without a deprecation period and telemetry
- Version only when truly breaking; prefer capability flags via headers
Performance checklist
- Index heavy filters and sorts; avoid full scans
- Batch writes, stream reads
- Compress JSON; prefer compact field names without losing clarity
- Measure P95 latency and tail behavior per route; budget SLOs
HTTP tips:
- Keep-alive and HTTP/2 multiplexing; coalesce domains
- Return 206 and stream for large exports; gzip/br all text
Security basics
- OAuth 2.0 with short-lived access tokens; rotate refresh tokens
- Least privilege scopes; audit logs for sensitive operations
- Rate limiting and anomaly detection at the edge
Threat basics:
- Validate and sanitize all inputs; centralize authn/z
- Deny by default; explicit allow lists for IPs or tenants as needed
Contracts and compatibility
Generate clients from OpenAPI to freeze contracts. Introduce enums cautiously—add unknown
handling on clients. Use x-deprecated
annotations and ship telemetry to measure usage before removal.
Documentation as a product
Keep examples real. Show curl, Postman, and typed SDK snippets. Version docs alongside code; publish previews for every PR. If users copy/paste and succeed, your API is winning.
Docs checklist:
- Example requests/responses for every route and error
- Filter/pagination examples with edge cases
- Rate limits and retry semantics documented per endpoint
Testing your API like a consumer
- Contract tests generated from OpenAPI (server and clients)
- Scenario tests for golden paths (signup → create → list → export)
- Negative tests for limits, malformed payloads, and auth errors
Observability and operations
- Request IDs and correlation IDs across services
- Structured logs with route, tenant, and response times
- Dashboards by endpoint: rate, error %, P95/P99 latency
- Slow query traces tied to endpoint names
Deprecation playbook
- Add new fields/routes; keep old ones working
- Announce with timeline; add
Deprecation
/Sunset
headers - Collect usage telemetry; contact high-traffic consumers
- Provide automated fixes or compatibility adapters
- Remove after the window closes; keep a rollback plan
Durable APIs aren’t accidental—they’re the result of small, consistent choices. Favor clarity over cleverness, keep contracts stable, and measure what matters.