Conventions on this page are framework-neutral. Modern defaults: JSON over HTTPS, OAuth 2 / Bearer
tokens, OpenAPI 3.1 as the schema, RFC 9457 for errors. Where conventions split (e.g. URL vs header
versioning) the row says so.
Malformed request. Reserve for parse failures, not validation.
401 Unauthorized
No / bad credentials. Set WWW-Authenticate.
403 Forbidden
Authenticated but not allowed.
404 Not Found
Resource doesn’t exist (or you can’t see it).
405 Method Not Allowed
Wrong verb on a known URI. Set Allow.
409 Conflict
State conflict (e.g. already cancelled).
410 Gone
Resource existed, no longer available.
412 Precondition Failed
If-Match didn’t match. Concurrency control.
415 Unsupported Media Type
Body content-type not accepted.
422 Unprocessable Entity
Preferred for validation failures.
429 Too Many Requests
Rate limited. Set Retry-After.
500 / 502 / 503 / 504
Server / gateway. Don’t leak stack traces.
query string conventionsQuerying & filtering
?status=shipped&customer_id=42
Equality filters. Combine with AND semantics.
?status=shipped,pending
Comma-separated for IN filters.
?created_at[gte]=2026-01-01
Operator suffix for ranges. Pick a syntax & stick with it.
?sort=-created_at,name
Leading - means descending. Multi-key support.
?fields=id,total,customer.id
Sparse fieldsets / projection.
?include=customer,items
Embed related resources. Cap depth.
?q=foo
Free-text search. Document the matching rules.
Booleans: true / false
Not 1/0. Coerce on the server.
Dates: ISO 8601 / RFC 3339
Always UTC, with offset. Document the expected format.
URL-encode everything user-supplied
Always. Search terms, ids, anything.
cursor · keyset · pagePagination
Cursor-based
Preferred Opaque token. Stable under inserts.
Keyset (since_id / before_id)
Fastest on large tables; needs a sort key.
Page-number
Avoid for hot data. Shifts under writes.
Link header (RFC 5988)
rel="next" / "prev". Standards-friendly.
total / has_next
Always say at least whether more exists. Skip total on huge sets.
Cap limit server-side
Hard cap (e.g. 100) + reasonable default (e.g. 20).
Deterministic sort
Always include the id in the sort to break ties.
bash
### Cursor — preferred for streams + stable order
GET /v1/orders?limit=50&cursor=eyJpZCI6MTAwfQ
{
"data": [ ... ],
"page_info": { "has_next": true, "cursor": "eyJpZCI6MTUwfQ" }
}
### RFC 5988 Link header — discoverable
Link: ; rel="next",
; rel="prev"
### Page-number — easy but bad with mutating data
GET /v1/orders?page=3&per_page=20
{ "data": [...], "page": 3, "total": 412, "per_page": 20 }
### Keyset — fastest under load (since_id / before_id)
GET /v1/orders?since_id=1500&limit=50
### Always:
- Cap `limit` server-side (e.g. 100 hard, 20 default)
- Encode cursors opaquely (base64(json)) and treat them as forward-compatible
- Document a deterministic sort order (e.g. -id) — pagination breaks without one
consistent shapeErrors
application/problem+json (RFC 9457)
Preferred Standardised error body.
type, title, status, detail, instance
Five canonical fields.
Field-level errors
Add an errors[] array with field + code + message.
Stable error codes
Strings, not numbers. order.already_shipped beats 42.
Trace id in body
Lets users send you something useful when they report bugs.
Don’t leak internals
Strip stack traces, query plans, internal hostnames before sending.
Distinguish 4xx vs 5xx carefully
Client-correctable vs server bug. Mis-classifying ruins alerting.
json
### RFC 9457 (Problem Details for HTTP) — the canonical error format
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/order-already-shipped",
"title": "Order already shipped",
"status": 409,
"detail": "Order 101 was shipped at 2026-05-17T12:00:00Z and can't be cancelled.",
"instance": "/v1/orders/101/cancel",
"trace_id": "abc123",
"errors": []
}
### Common shapes
- type — stable URI per error class
- title — short human-readable summary (same per type)
- status — repeat of HTTP status (helps proxies / logs)
- detail — human-readable, specific to this instance
- instance — URI of the failing request
- errors — array for field-level validation faults
Adding fields / new endpoints / optional params. Ship without bumping.
Breaking changes
Field renames / type changes / removals. New major version.
Deprecation: header
RFC 8594 Deprecation + Sunset headers.
Compatibility window
Promise N-1 (or N-2) versions on the docs page.
bash
### URL versioning — most common, easiest to debug
GET /v1/orders/101
GET /v2/orders/101
### Header / media-type versioning — keeps URLs stable
GET /orders/101
Accept: application/vnd.example.api+json;version=2
### Query versioning — fine for internal APIs, weak for caching
GET /orders/101?api_version=2
### Date-based versioning (Stripe-style)
GET /orders/101
Stripe-Version: 2025-05-17
### Rules of thumb
- Adding fields is non-breaking. Removing or changing types is breaking.
- Bumping versions costs the team weeks; do it for shape changes, not field renames.
- Deprecation: announce, ship the new version, keep the old for 6-12 months,
serve `Deprecation: true` + `Sunset: ` headers, then remove.
identity & accessAuth & security
Authorization: Bearer …
OAuth 2 access tokens. Standard for service-to-service + user-on-behalf.
Authorization: Basic …
Legacy Username:password. Use over TLS only.
API keys in headers, never URLs
URLs leak in logs, referers, browser history.
JWT vs opaque tokens
JWT for stateless verify; opaque tokens for revocability.
Scopes / permissions
Least-privilege per token. Document required scope per endpoint.
CORS
Whitelist origins explicitly. Never * with credentials.
Caller-supplied UUID. Server dedupes retries within a window.
If-Match: "abc"
Optimistic concurrency for PUT/PATCH/DELETE.
If-None-Match: "abc"
Conditional GET. 304 saves bandwidth.
ETag: "abc"
Strong validator. Hash of the representation.
Last-Modified / If-Modified-Since
Weaker, time-based variant.
Cache-Control: private, max-age=15
Per-user cacheable for 15 s.
Cache-Control: public, max-age=300
CDN-cacheable shared resource.
Vary: Authorization, Accept-Language
Tell caches what varies the response.
links in responsesHypermedia
Plain link fields
Most pragmatic. { "url": "/v1/orders/101" }.
HAL (application/hal+json)
_links + _embedded envelope.
JSON:API (application/vnd.api+json)
Spec-driven envelope with relationships.
Siren / Collection+JSON
Older link-rich formats. Rare in new APIs.
Discoverability via Link header
Pagination, schemas, profiles — cheap and standards-friendly.
OpenAPI > HATEOAS
For most teams, a published OpenAPI doc beats runtime hypermedia.
Pure HATEOAS APIs are rare in practice. The pragmatic middle: include URLs for related resources in the
response so clients don’t hand-assemble them, but document everything in OpenAPI.
a production-shaped resourceEnd-to-end · Order lifecycle
List with ETag-based caching, create with idempotency, conditional GET, partial update with optimistic
concurrency, and a domain-error response. The pattern most APIs converge on.
bash
### A minimal but production-shaped order resource
# List with filters + pagination
GET /v1/orders?status=shipped&limit=20
Authorization: Bearer ...
200 OK
ETag: "abc123"
Cache-Control: private, max-age=15
# Create with idempotency
POST /v1/orders
Authorization: Bearer ...
Idempotency-Key: 8f3a-...
Content-Type: application/json
{ "customer_id": 42, "items": [...] }
201 Created
Location: /v1/orders/101
{ "id": 101, "status": "pending", ... }
# Read with conditional GET
GET /v1/orders/101
If-None-Match: "abc123"
304 Not Modified
# Partial update
PATCH /v1/orders/101
If-Match: "abc123"
Content-Type: application/merge-patch+json
{ "status": "cancelled" }
200 OK
ETag: "def456"
# Domain error
POST /v1/orders/101/cancel
409 Conflict
Content-Type: application/problem+json
{ "type": "...", "title": "Already shipped", "status": 409 }
Best practiceGood to know
Wrap collections in an envelope.{ "data": […], "page_info": { … } } gives you a place to add
pagination, errors, and meta later without breaking clients. Bare top-level arrays paint you into a
corner.
Idempotency keys on every write.
Cheap to add (Idempotency-Key header, server-side dedupe in a 24-hour
window). Eliminates the worst class of duplicate-charge bugs in retry-happy clients.
Publish an OpenAPI doc and lint it in CI.
Spectral or Redocly rules catch missing examples, inconsistent error shapes, and breaking changes
before they ship. Half the value of REST design is uniformity.
Common trapsWatch out for
200 OK on errors.
Returning { "error": "…" } with status 200 breaks every
monitoring tool. Match the status to the outcome.
Auth in the URL.?api_key=… ends up in proxy logs, browser history, referer headers.
Use a header.
Inconsistent error shape across endpoints.
Clients write a switch statement and ship a hidden coupling. Pick a single error format (RFC 9457 is
the safe default) and enforce it via linting.
REST (Representational State Transfer) is an architectural style for designing networked APIs over HTTP. A REST API models data as resources identified by URLs, uses standard HTTP verbs (GET, POST, PUT, PATCH, DELETE) for operations, and returns responses in JSON or another media type.
What HTTP status codes should a REST API return?
200 OK for successful reads, 201 Created with a Location header for new resources, 204 No Content for successful deletes, 400 for invalid input, 401 when credentials are missing, 403 when access is denied, 404 Not Found, 409 Conflict for duplicates, and 422 for validation failures.
How should REST APIs handle errors?
Use RFC 9457 Problem Details format: return application/problem+json with a type URI, title, status code, and a detail message. Include a validation_errors array for field-level errors. Consistent error shapes let clients display and log errors without parsing free-text messages.
What are the best practices for REST API versioning?
The three common strategies are URL path versioning (/v1/orders), Accept header versioning (Accept: application/vnd.api+json;version=1), and query-parameter versioning (?version=1). URL versioning is the most visible and easiest to test in a browser; header versioning keeps URLs clean.
What is idempotency in REST APIs?
An idempotent operation produces the same result whether called once or many times. GET, PUT, and DELETE are idempotent by definition; POST is not. For non-idempotent POST endpoints, accept an Idempotency-Key header and cache the first response so retries do not cause duplicate side effects.