REST API Design#
Problem statement (interviewer prompt)
Design the REST API for a payments service: idempotent POSTs with Idempotency-Key, cursor pagination, structured errors via RFC 7807 Problem Details, ETag-based conditional reads, and a versioning policy with sunsetting.
flowchart LR
C([Client])
R[Resource URL]
V[HTTP verb<br/>GET POST PUT PATCH DELETE]
S[HTTP status<br/>2xx 4xx 5xx]
E[Error envelope]
C --> R --> V --> S
S --> E
classDef p fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef s fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
class C p;
class R,V,S,E s;
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class C client;
class R,V,S,E service;
Good REST: nouns in URLs, verbs in HTTP methods, status codes that mean what they say, idempotent writes, cursor pagination, structured errors, and an explicit versioning policy.
Resource model#
/orders collection
/orders/{id} one order
/orders/{id}/items sub-collection
/orders/{id}/items/{lid} one item
/customers/{id}/orders nested view of a customer's orders
Nouns, plural. URLs are stable; verbs are HTTP.
Verb cheat sheet#
| Verb | Idempotent | Safe | Used for |
|---|---|---|---|
| GET | yes | yes | read |
| HEAD | yes | yes | metadata only |
| OPTIONS | yes | yes | CORS / capabilities |
| PUT | yes | no | replace whole resource |
| DELETE | yes | no | remove |
| POST | no | no | create / non-idempotent action |
| PATCH | not by default | no | partial update |
"Idempotent" = same effect if repeated → safe to retry on network errors.
Status codes you'll actually use#
| Code | Meaning |
|---|---|
| 200 OK | read / non-creating success |
| 201 Created | new resource; Location: header points at it |
| 202 Accepted | async work queued |
| 204 No Content | success with empty body (DELETE) |
| 400 Bad Request | validation / parse error |
| 401 Unauthorized | missing or invalid auth |
| 403 Forbidden | authenticated but not allowed |
| 404 Not Found | resource doesn't exist |
| 405 Method Not Allowed | wrong verb for this URL |
| 409 Conflict | optimistic concurrency / business conflict |
| 410 Gone | sunset version |
| 422 Unprocessable | well-formed but semantically invalid |
| 429 Too Many Requests | rate limited; Retry-After |
| 500 Internal Server Error | bug |
| 502/503/504 | gateway / upstream / timeout |
Error envelope (RFC 7807 Problem Details)#
{
"type": "https://errors.example.com/insufficient_funds",
"title": "Insufficient funds",
"status": 422,
"code": "INSUFFICIENT_FUNDS",
"detail": "Account 42 has balance 5, requested withdrawal 100",
"instance": "/withdrawals/abc-123",
"trace_id": "0a8b...",
"errors": [
{"field": "amount", "code": "TOO_LARGE"}
]
}
Stable code for programmatic handling; trace_id so support can find the log line.
Idempotency#
sequenceDiagram
participant C as Client
participant S as Server
participant DB as Idempotency table
C->>S: POST /charges + Idempotency-Key: abc
S->>DB: lookup abc
alt new
DB-->>S: not found
S->>S: do work
S->>DB: store (abc, response, 200)
S-->>C: 200 + result
else seen
DB-->>S: cached response
S-->>C: same response
end
Client supplies Idempotency-Key header. Server stores (key → response, status) for 24-48 h. See idempotency-retries.
Pagination#
| Style | Pros | Cons |
|---|---|---|
Offset (?offset=200&limit=50) |
trivial | skips/duplicates when data changes |
Page-based (?page=4) |
same as offset, prettier | same drift |
Cursor (?cursor=opaque) |
stable, scales | random access hard |
Time-based (?since=ts) |
natural for events | gaps if events share ts |
Use cursor by default; next_cursor in the response.
Filtering / sorting / sparse fieldsets#
Keep filters typed (status=PAID not status="paid"). Sort prefixes: - desc.
Search vs lookup#
- Lookup by id:
/orders/{id}→ 404 if absent. - Search:
/orders?status=...&q=...→ 200 with possibly empty list.
Bulk actions#
POST /orders/bulkwith array.- Return per-item result with HTTP 200 +
errors[](don't fail the whole batch on one bad row).
Long-running operations#
POST /reports → 202 Accepted, Location: /operations/op_1
GET /operations/op_1 → status: running | done | failed
GET /operations/op_1/result → final payload when done
Caching headers#
ETagfor conditional GET (If-None-Match).Last-Modified/If-Modified-Since.Cache-Control: public, max-age=...,s-maxage=...for shared caches.
Authn / Authz#
- Authentication: Bearer JWT or session cookie.
- Authorization: per-resource checks (404 vs 403 - 404 leaks less).
- mTLS for B2B.
Versioning#
Anti-patterns#
- Verbs in URLs (
/getOrder?id=...). - Returning 200 with
{ "error": "..." }- use proper status. - Mixing snake_case + camelCase in the same payload.
nullfor "field unchanged" in PATCH - use JSON Merge Patch or JSON Patch standard.- Exposing primary keys without context - prefix with type (
ord_abc).
Richardson Maturity Model#
- L0: tunnels everything over POST.
- L1: resources at URLs.
- L2: HTTP verbs + statuses (most APIs).
- L3: HATEOAS - hypermedia links in responses (rare in practice).
L2 is the realistic target.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
LSM vs B-Tree engines | WAL, memtable, SSTables, compaction | storage-engines-lsm-btree |
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
HLD |
Service mesh | sidecar mesh, mTLS, traffic policy | service-mesh |
HLD |
API versioning & evolution | URI / header versioning, sunsetting | api-versioning-evolution |
LLD |
REST API design | verbs, statuses, pagination, errors | rest-api-design |
Quick reference#
Spec / contract first#
- OpenAPI 3 in a versioned repo.
- Code generators for client SDKs (typescript-axios, openapi-generator).
- CI lints: spectral, redocly.
Naming rules#
- camelCase for JSON keys (most ecosystems) or snake_case (Stripe). Pick one.
- Plural collection nouns (
/orders,/users). - Lowercase URLs.
- ISO 8601 timestamps in UTC.
- Money as
{amount: string, currency: "USD"}(string to avoid float).
Pagination cursors#
- Opaque base64. Encode
(table, last_id, sort_value). - Never expose database internals (rowid).
Backward compat#
- Add optional fields, never remove.
- Add new enum values - clients must tolerate.
- New required headers → new version.
Refs#
- "RESTful Web APIs" - Richardson & Amundsen.
- "API Design Patterns" - JJ Geewax.
- Google AIPs (aip.dev).
- Stripe API docs (gold standard).
- RFC 7807 (Problem Details), 8594 (Sunset), 6749 (OAuth2).
FAQ#
What is the difference between PUT and PATCH in REST?#
PUT replaces the entire resource representation, while PATCH applies a partial update. Both should be idempotent so retries do not change the outcome.
How do I make a POST request idempotent?#
Accept an Idempotency-Key header from the client. Store the first response keyed by that header so retries return the same result without re-executing side effects.
Should I use cursor or offset pagination?#
Cursor pagination scales better because it does not skip rows. Offset becomes slow as page numbers grow and yields duplicates when items shift between pages.
How should I version a REST API?#
Put the major version in the URL (e.g. /v1/) and reserve breaking changes for new majors. Communicate sunset dates with the Deprecation header well ahead of removal.
What is RFC 7807 Problem Details?#
A standard JSON shape for HTTP errors with type, title, status, detail, and instance fields. Using it gives clients a predictable structure for all failures.
Related Topics#
- API Gateway: the infrastructure layer that enforces REST contracts, versioning, and rate limits
- SOLID Principles: LSP and ISP shape resource contracts and response shapes in REST APIs
- Error Handling: HTTP status codes, problem+json, and structured error responses
Further reading#
Curated, high-credibility sources for going deeper on this topic.