Chapter 02 / 24 · System Design

Designing the API

The API outlives every implementation behind it; design it like the permanent thing it is.

Databases get migrated, services get rewritten, whole architectures get replaced — and through all of it, the API keeps its promises, because clients you do not control depend on every one of them. That asymmetry is the whole discipline: the API is the longest-lived artefact you will design, so design it like the permanent thing it is.

REST that clients can guess

The convention that won did so by being guessable: nouns for resources (/orders/123, not /getOrder), verbs from HTTP (GET reads and is cacheable, POST creates, PUT/PATCH replace/modify, DELETE deletes), and status codes that mean what they say — 2xx success, 4xx “you did something wrong, don’t retry as-is”, 5xx “we did, retrying is reasonable”. That last split is not pedantry: it is the retry policy of every client you will ever have, encoded in a number. Return 500 for a validation error and well-behaved clients will politely hammer you with the same bad request forever.

The contract details that decide everything later

DecisionDo thisBecause
Paginationopaque cursors, not offsetsoffset 100000 scans 100000 rows and skips/duplicates under concurrent writes; a cursor is O(1) at the index and stable
Unsafe operationsaccept an idempotency keythe Distributed Systems rules: clients WILL retry; make the second attempt return the first outcome
Errorsmachine-readable body: code, message, retryable?clients branch on your errors; give them fields, not prose to regex
Limitsdocument timeouts, rate limits, max sizes in the contractundocumented limits are landmines with your logo on them

Evolving without breaking anyone

The compatibility rules are the RPC chapter’s, applied at the public edge: adding an optional field or a new endpoint is safe; renaming, removing, retyping, or changing the meaning of anything is a break — and the uncomfortable law is that any observable behaviour will be depended on, error text included. When a break is truly unavoidable: version (/v2/ at the path is crude and perfectly effective), run both, announce deprecation with dates, watch v1 traffic decay, and only then retire it. Companies that respect this loop keep developer trust for decades; the shortcut is remembered for exactly as long.

When REST is the wrong shape

gRPC earns its place service-to-service: typed contracts, deadline propagation, streaming — internal edges where you control both sides. GraphQL earns its place when many differently-shaped frontends over-fetch from one API — clients ask for exactly the fields they need; the bill is server complexity (every query is a little planner, caching gets harder, one endpoint hides expensive queries). The honest default remains boring REST + JSON, upgraded when a measured pain — not fashion — names one of these shapes.