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, notPOST /createPhoto. - Plural, hierarchical paths.
/users/{id}/photosreads like the relationship it represents. - The verb carries the intent.
GETreads,POSTcreates,PUT/PATCHupdate,DELETEremoves.
Make verbs mean what they say
The value of HTTP verbs is their guarantees, which clients and infrastructure rely on:
| Verb | Use | Safe? | Idempotent? |
|---|---|---|---|
GET | Read | Yes | Yes |
POST | Create / actions | No | No |
PUT | Replace | No | Yes |
PATCH | Partial update | No | No* |
DELETE | Remove | No | Yes |
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 stablecode, never the human message. - Lean on status codes.
200ok,201created,400bad input,401no session,403not allowed,404missing,409conflict,429rate-limited,5xxyour fault.
Paginate everything that can grow
Never return an unbounded list. Two common approaches:
| Approach | How | Good for |
|---|---|---|
| Offset | ?page=3&limit=20 | Small, stable datasets; jump-to-page |
| Cursor | ?after=<opaque-cursor>&limit=20 | Large, 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 /ridesto request a ride,GET /rides/{id}to poll its status, andGET /drivers/{id}/ridesfor 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.
