Learn/system design/API Design
Intermediate~25 min read

API Design

Designing clean APIs — REST maturity, HTTP semantics, versioning, pagination, idempotency, error formats, auth, and REST vs GraphQL vs gRPC.

RESTgRPCVersioningPagination

What Makes an API Good

A good API is predictable, consistent, and hard to misuse. Consumers should be able to guess the next endpoint from the last one. The core levers are resource naming, correct use of HTTP semantics, sensible errors, stable versioning, and clear auth. This page focuses on HTTP/REST with a comparison of GraphQL and gRPC.

REST & the Richardson Maturity Model

REST treats everything as resources manipulated through a uniform interface. Richardson's maturity model grades how RESTful an API is:

  1. Level 0 — one endpoint, RPC over HTTP (SOAP-style).
  2. Level 1 — multiple resources (/orders, /users) but verbs ignored.
  3. Level 2 — proper HTTP verbs and status codes. This is where most good APIs live.
  4. Level 3 — HATEOAS: responses carry links to related actions.

Resource Naming

Use plural nouns, lowercase, hyphenated, and hierarchical. The URL identifies a thing; the HTTP method says what to do to it. Verbs in the path (/getUser) are an anti-pattern.

http
GET    /users                 list users
POST   /users                 create a user
GET    /users/42              get user 42
PATCH  /users/42              partial update
DELETE /users/42              delete
GET    /users/42/orders       orders belonging to user 42
GET    /orders?status=paid&sort=-created_at   filter + sort

HTTP Methods, Idempotency & Status Codes

MethodSafeIdempotentUse
GETyesyesRead
POSTnonoCreate / actions
PUTnoyesReplace whole resource
PATCHnonot requiredPartial update
DELETEnoyesRemove

Idempotent means repeating the call has the same effect as calling once — critical because clients retry on timeouts. Status codes carry meaning:

CodeMeaning
200 / 201 / 204OK / Created / No Content
301 / 304Moved / Not Modified (cache hit)
400 / 401 / 403Bad request / Unauthenticated / Forbidden
404 / 409 / 422Not found / Conflict / Unprocessable
429Too Many Requests (rate limited)
500 / 502 / 503Server error / Bad gateway / Unavailable

4xx vs 5xx

4xx = the client did something wrong (don't retry blindly). 5xx = the server failed (safe to retry with backoff). Never return 200 with an error body — it breaks caching, monitoring, and client logic.

Versioning

StrategyExampleTrade-off
URI path/v2/usersObvious, cacheable; not purely RESTful
HeaderAPI-Version: 2Clean URLs; harder to test in browser
Media typeAccept: application/vnd.api.v2+jsonMost RESTful; least discoverable

URI versioning is the pragmatic default. Whatever you pick, version only on breaking changes; additive changes (new optional fields) should never bump the version.

Pagination, Filtering & Sorting

StyleQueryNotes
Offset?limit=20&offset=40Simple; slow & skips rows on deep/changing data
Cursor / keyset?limit=20&after=eyJpZCI6...Stable, fast at scale; can't jump to page N

Use cursor pagination for large or real-time datasets — offset pagination degrades and duplicates/skips rows as data shifts underneath. Support ?sort=-created_at,name (leading - = descending) and field filters like ?status=active.

Idempotency Keys

POST is not idempotent, yet clients retry. Let the client send a unique Idempotency-Key header; the server stores the first result keyed by it and replays that result for any retry — so a double-charge becomes impossible. This is how Stripe handles payment retries.

Rate Limiting

Return 429 Too Many Requests when a client exceeds its quota, and tell it when to come back with Retry-After. Advertise the quota so well-behaved clients self-throttle:

http
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Retry-After: 30

Error Format — RFC 9457 (problem+json)

Standardize errors so clients parse them uniformly. RFC 9457 (Problem Details for HTTP APIs, obsoleting 7807) defines the shape:

http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "email must be a valid address",
  "instance": "/users",
  "errors": [{ "field": "email", "code": "invalid_format" }]
}

Authentication

SchemeBest for
API keysServer-to-server, simple identification (not fine-grained authz)
OAuth 2.0Third-party delegated access, scopes, refresh tokens
JWT (bearer)Stateless sessions; short-lived, signed, verified per request

Always over TLS. Keep JWTs short-lived and pair with refresh tokens; never put secrets in the query string.

Caching Headers

Cache-Control tells clients/CDNs how long a response is fresh. ETag is a content fingerprint; the client sends it back as If-None-Match, and an unchanged resource returns 304 Not Modified with no body — saving bandwidth. ETags also enable optimistic concurrency (send If-Match on writes to avoid lost updates).

REST vs GraphQL vs gRPC

 RESTGraphQLgRPC
TransportHTTP/1.1 JSONHTTP + JSONHTTP/2 + Protobuf
FetchingOver/under-fetchClient picks fieldsFixed methods
PerfGoodGoodFastest, streaming
Best forPublic APIs, CRUDRich clients, many entitiesInternal microservices
CachingHTTP nativeHard (all POST)App-level

gRPC defines services in a .proto file, generating typed client/server stubs and using compact binary Protobuf over HTTP/2 with bidirectional streaming — ideal east-west between services but awkward from browsers (needs gRPC-Web).

Webhooks & Gateways

Webhooks invert the flow: instead of the client polling, your server POSTs an event to the client's URL. Sign the payload (HMAC) so receivers can verify authenticity, include an event ID for dedup, and retry with backoff on non-2xx. An API gateway fronts everything, handling auth, rate limiting, routing, and observability in one place.

A Well-Designed Endpoint

http
POST /v1/orders
Authorization: Bearer <jwt>
Idempotency-Key: 9c8b-...-a1
Content-Type: application/json

{ "items": [{ "sku": "ABC", "qty": 2 }], "currency": "USD" }

--- response ---
HTTP/1.1 201 Created
Location: /v1/orders/ord_7f3
ETag: "a1b2c3"
Cache-Control: private, max-age=0

{
  "id": "ord_7f3",
  "status": "pending",
  "total": 4200,
  "_links": { "self": "/v1/orders/ord_7f3",
              "cancel": "/v1/orders/ord_7f3/cancel" }
}

Bottom line

Nouns for resources, verbs from HTTP, precise status codes, idempotency keys on writes, cursor pagination, problem+json errors, and versions only on breaking changes. Consistency beats cleverness.

Section navigation