A REST API is a contract: clear to call, safe to retry, changeable without breaking existing clients.
Resources and methods
Model nouns as resources with stable IDs; let the method carry the verb. Keep paths plural and hierarchical (/orders/{id}/items), never /createOrder.
| Method | Meaning | Safe | Idempotent |
|---|---|---|---|
GET /orders/{id} | Read one | yes | yes |
GET /orders | List | yes | yes |
POST /orders | Create | no | no |
PUT /orders/{id} | Replace | no | yes |
PATCH /orders/{id} | Partial update | no | no |
DELETE /orders/{id} | Remove | no | yes |
- Safe: no side effects, cacheable.
- Idempotent: repeating leaves the same server state, so retry after a timeout is fine.
- POST is neither, so a dropped response after a create is dangerous.
Request chain (cheap rejections first)
Each stage can reject before expensive work runs.
Client -> Gateway (TLS, routing) -> AuthN/AuthZ -> Rate limit
-> Validate/parse -> Handler -> Data store -> Response
Auth fails before rate limiting spends a lookup; rate limiting rejects before validation parses; validation catches bad input before the handler hits the store.
Status codes
- 2xx:
200 OKread/update,201 Created(returnLocation),202 Accepted(async),204 No Content(delete). - 4xx:
400malformed,401missing/invalid creds,403authenticated-but-not-allowed,404missing/hidden,409state clash (dup create),422semantically invalid,429rate limited. - 5xx: server fault.
- Retry
5xxand429; never blindly retry other4xx(the request itself is wrong).
Consistent error body
Same machine-readable shape everywhere so clients branch without parsing prose.
code: stable enum the client switches on.message: prose for a human.request_id: lets support trace one call in the logs.details: optional per-field issues.
Pagination (never unbounded)
- Offset/limit (
?limit=50&offset=100): simple, jumpable; slow on deep pages (DB walks and discards skipped rows), unstable under inserts/deletes. - Cursor (
?limit=50&cursor=...): opaque pointer to last item; stable under writes, maps to indexed range scan. Prefer for large or fast-changing collections. - Filter/sort via query params (
?status=paid&sort=-created_at); each filterable field implies an index.
Versioning
- Put major version in path (
/v1/orders) for visibility and cache-friendliness. - Additive = compatible: new endpoint, new optional request field, new response field.
- Subtractive = breaking: remove/rename a field, change a type, make optional required, change default behavior.
- Deprecate on a timeline: announce, emit
Deprecationheader, remove after the window.
Auth: authenticate then authorize
Authentication = who you are; authorization = what you may do (in that order).
- API keys: identify a caller, good server-to-server; scope and rotate.
- OAuth 2.0 / OIDC: short-lived access tokens (+ refresh) for user-facing and third-party.
- JWT: self-contained signed token, no DB lookup; fast but hard to revoke, so keep lifetime short.
- Enforce authorization in the service layer, never only the client.