Skip to content

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.

Concept illustration
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#

GET /orders?status=PAID&min_total=100&sort=-created_at&fields=id,total

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/bulk with 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#

  • ETag for 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#

See api-versioning-evolution.

Anti-patterns#

  • Verbs in URLs (/getOrder?id=...).
  • Returning 200 with { "error": "..." } - use proper status.
  • Mixing snake_case + camelCase in the same payload.
  • null for "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.

  • 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.