Core Concepts

API Design

How to design an interface others actually enjoy building against — resources and verbs, request/response shapes, pagination, idempotency, versioning, and the small decisions that make an API feel solid.


An API is a contract. Once someone builds against it, changing it costs everyone. Good API design is mostly about making that contract predictable — so a developer can guess the next endpoint correctly without reading the docs.

In a system design interview, the API is where you turn vague requirements into something concrete. Nail down the handful of operations your system supports and most of the design follows.

Start from the operations, not the tables

Resist the urge to expose your database. Start by listing what users actually do, then name a resource and a verb for each:

"users can post a photo"          → POST   /photos
"users can see a photo"           → GET    /photos/{id}
"users can list a user's photos"  → GET    /users/{id}/photos
"users can delete their photo"    → DELETE /photos/{id}

A few rules keep this clean:

  • Nouns for resources, HTTP verbs for actions. POST /photos, not POST /createPhoto.
  • Plural, hierarchical paths. /users/{id}/photos reads like the relationship it represents.
  • The verb carries the intent. GET reads, POST creates, PUT/PATCH update, DELETE removes.

Make verbs mean what they say

The value of HTTP verbs is their guarantees, which clients and infrastructure rely on:

VerbUseSafe?Idempotent?
GETReadYesYes
POSTCreate / actionsNoNo
PUTReplaceNoYes
PATCHPartial updateNoNo*
DELETERemoveNoYes

Safe means no side effects (a crawler can call it freely). Idempotent means calling it twice is the same as once — which matters the moment the network is unreliable, because clients retry.

Design for retries from day one

Networks drop responses. A client that sent POST /payments, got no reply, and retries must not charge the card twice. The fix is an idempotency key — a unique id the client generates and sends with the request:

POST /payments
Idempotency-Key: 7f3c-...-a91b
{ "amount": 5000, "currency": "UGX" }

The server records the key with the result. A repeat of the same key returns the original result instead of doing the work again. Any create that must happen exactly once needs this.

Shape responses for the client

  • Return what the caller needs, not your whole row. Trim internal fields; add computed ones (fullName, isOwner).
  • Use consistent envelopes. Pick a shape — { "data": ..., "meta": ... } on success, { "error": { "code", "message" } } on failure — and never deviate. Clients switch on the stable code, never the human message.
  • Lean on status codes. 200 ok, 201 created, 400 bad input, 401 no session, 403 not allowed, 404 missing, 409 conflict, 429 rate-limited, 5xx your fault.

Paginate everything that can grow

Never return an unbounded list. Two common approaches:

ApproachHowGood for
Offset?page=3&limit=20Small, stable datasets; jump-to-page
Cursor?after=<opaque-cursor>&limit=20Large, changing datasets; feeds

Cursor pagination wins at scale: it's stable when rows are inserted mid-scroll (offset pagination skips or repeats items when the list shifts under it) and it doesn't get slower on deep pages. Return the next cursor in the response and let the client pass it back.

Version before you need to

The day you must make a breaking change, you'll be glad the version was already there. Put it in the path (/v1/photos) — it's blunt, visible, and easy to route. Then treat additive changes (new optional fields, new endpoints) as non-breaking and ship them freely on the same version; reserve a version bump for removing or renaming things.

What to say in an interview

Lead with the operations, write them as resource + verb, and call out the two things interviewers look for: how you paginate large lists and how you make writes safe to retry. A clean answer sounds like:

"The core API is POST /rides to request a ride, GET /rides/{id} to poll its status, and GET /drivers/{id}/rides for history. The create takes an idempotency key so a retried request doesn't book two rides. History is cursor-paginated since it grows forever. Everything's versioned under /v1."


Next up: Data Modeling — choosing the database and schema those endpoints sit on.