API design — REST, idempotency, versioning
📖 Walk me through it — plain English
An API (Application Programming Interface) is the set of "doors" one program opens so other programs can ask it to do things — fetch some data, create a record, delete something. REST is just a popular style of arranging those doors around resources (the nouns your system owns, like users or orders) plus a small fixed set of verbs (the actions: GET, POST, PUT, PATCH, DELETE). This lesson is the checklist an interviewer wants when they say "design the endpoints for X": pick the right nouns, the right verb, the right status code (a 3-digit number the server returns to say how it went), and then show you know the four "grown-up" concerns — idempotency, pagination, versioning, and clean errors.
Analogy: think of your API as the order counter at a busy restaurant. The menu items are the resources (nouns). What you're allowed to do — look, order, change, cancel — are the verbs. The cook shouting back "got it / sold out / kitchen's on fire" is the status code. Most of the design work is just agreeing, ahead of time, on what each word means so the customer and the kitchen never get confused.
Two terms the lesson leans on, defined plainly. Safe means the call only reads, it never changes anything — like reading the menu. Idempotent means doing it once or doing it five times leaves the world in the same state. Cancelling an order is idempotent: cancel it once or hammer "cancel" ten times, it ends up cancelled either way. But "place a new order" is not idempotent — press it five times and you get five dinners. That single idea explains most of the verb rules:
The error codes in the table are worth memorizing because they sound similar but mean different things. 401 = "I don't know who you are" (not logged in). 403 = "I know who you are, and you're not allowed" (logged in, wrong permissions). 409 = "conflict" (e.g. that username is taken). 422 = "your input is shaped wrong" (validation failed). 429 = "you're calling too fast" (rate-limited). Returning the right one is the difference between a client that can react sensibly and one that's left guessing.
Why the four extras matter, in one breath each. Idempotency keys: the client attaches Idempotency-Key: some-uuid to a POST; the server remembers that key's result for ~24h, so if the network hiccups and the client retries, the payment runs once, not twice. Pagination: don't return a million rows at once — hand back a page plus a cursor (a bookmark, ?after=cursor&limit=50); cursors beat counting by offset because offsets shift when rows are added or removed mid-scroll. Versioning: once people depend on /v1/, you never silently change it — you ship a new /v2/ beside it. Errors: every failure comes back in the same JSON shape with a stable machine-readable code, and never a raw stack trace (that leaks internals and is a security risk).
How to attack "design the API for X" in an interview:
- Name the resources (nouns) first, e.g.
/orders,/orders/123/items. - Map each action to a verb + status code using the table above — and say out loud whether it's safe/idempotent.
- Call out idempotency keys for any "create" that money or retries touch.
- Add pagination, filtering/sorting on list endpoints; reject unknown params so you don't owe forever-compatibility.
- Mention versioning and a consistent error shape, and only reach for GraphQL if clients genuinely need wildly different data shapes.
It "works" because everyone — client, server, and the caching/proxy layers in between — already agrees on what the verbs and codes mean. That shared vocabulary is the whole point: predictable doors that behave the same way every time, even when calls get retried or the dataset shifts underneath you.
"Design the endpoints for X" shows up in mid/senior loops. They want resource modeling, correct verbs and status codes, and awareness of idempotency + pagination + versioning.
First, the vocabulary — every term you need, defined once
Before any design, lock down the words. None of these are hard once you see them in plain English, but interviewers notice when you use them precisely. Read this list once and the rest of the lesson is just applying it.
- API — a contract for talking to a program over the network. A web API speaks HTTP (HyperText Transfer Protocol — the request/response protocol the web runs on) and usually trades JSON (a plain-text data format of keys and values, e.g.
{"id":7}). - REST (Representational State Transfer) — a design style: model your system as resources addressed by URLs, and act on them with the standard HTTP verbs. It is a convention, not a library you install.
- Resource — a "thing" your system owns that has an identity: a user, an order, an invoice. Each has a URL (e.g.
/users/42). A collection is the plural URL holding many of them (/users). - HTTP method (verb) — the action:
GETread,POSTcreate,PUTreplace,PATCHpartial-update,DELETEremove. - Status code — the server's 3-digit verdict. 2xx = it worked, 3xx = go look elsewhere, 4xx = you (the caller) made a mistake, 5xx = the server broke.
- Idempotency — doing the same call N times has the same effect as doing it once. GET/PUT/DELETE are naturally idempotent; POST is not.
- Statelessness — the server keeps no memory of you between calls; every request must carry everything needed to handle it (including who you are). This is what lets you put ten identical servers behind a load balancer — any of them can answer any request.
- Pagination — returning a big list in small slices ("pages") instead of all at once. Offset = "skip the first N"; cursor = "give me what comes after this bookmark."
- Versioning — labelling the API (
/v1/) so you can change it later without breaking callers who depend on today's behaviour. - Rate limiting — capping how many calls a client may make per window (e.g. 100/min) to protect the service; over the cap you return
429. - Authentication (authn) — proving who you are. Authorization (authz) — deciding what you're allowed to do. (Hence 401 vs 403.)
- Schema — the agreed shape of a request or response body: which fields exist, their types, which are required.
- GET — safe + idempotent. Never mutates. 200 OK / 404 Not Found.
- POST — create or non-idempotent action. 201 Created (with Location header) / 400 Bad Request.
- PUT — replace whole resource. Idempotent. 200 / 204 No Content.
- PATCH — partial update. 200 / 204.
- DELETE — idempotent. 204 / 404.
- 401 not authenticated · 403 authenticated but not allowed · 409 conflict · 422 validation failed · 429 rate-limited.
A concrete, well-designed resource API
Theory clicks once you see real endpoints. Here is a small but complete design for users and their orders. Notice the pattern: plural nouns for collections, an id in the path to address one item, and nesting to express "belongs to" (an order belongs to a user). The verb — not the URL — says what you're doing, so you never write /createUser or /getUserOrders; the method already carries that meaning.
# Collection of users
GET /users # list users (paginated) -> 200
POST /users # create a user -> 201 + Location: /users/42
# A single user (the {id} is that user's identity in the path)
GET /users/42 # read one user -> 200 or 404
PUT /users/42 # replace the whole user -> 200 / 204
PATCH /users/42 # change only the fields sent -> 200 / 204
DELETE /users/42 # remove the user -> 204 or 404
# Orders that BELONG TO a user — nesting shows the relationship
GET /users/42/orders # this user's orders (paginated)-> 200
POST /users/42/orders # place an order for this user -> 201
GET /users/42/orders/9001 # read one order -> 200 or 404
Now a full create, end to end. The client sends a request (method + URL + headers + JSON body); the server returns a response (status code + headers + JSON body). Note the Idempotency-Key header on the write, and the Location header pointing at the freshly-made resource:
# --- REQUEST ---
POST /users/42/orders
Authorization: Bearer eyJhbGci... # who I am (a token, see Auth below)
Idempotency-Key: 6f1c-...-a3 # makes retrying this POST safe
Content-Type: application/json
{
"sku": "BOOK-001",
"quantity": 2
}
# --- RESPONSE ---
HTTP/1.1 201 Created
Location: /users/42/orders/9001 # URL of the thing just created
Content-Type: application/json
{
"id": 9001,
"user_id": 42,
"sku": "BOOK-001",
"quantity": 2,
"status": "pending",
"created_at": "2026-06-03T10:00:00Z"
}
And a list call, showing pagination and filtering on the same collection. The response wraps the array in an object so there's room for paging metadata next to the data — returning a bare top-level array is a classic trap, because you can never add fields beside it later without breaking clients.
# --- REQUEST ---
GET /users/42/orders?status=pending&sort=-created_at&limit=2
# --- RESPONSE ---
HTTP/1.1 200 OK
{
"data": [
{ "id": 9001, "status": "pending" },
{ "id": 9000, "status": "pending" }
],
"next_cursor": "eyJpZCI6OTAwMH0", # pass as ?after=... for the next page
"has_more": true
}
The error format (one shape, every time)
Pick a single JSON error envelope and use it for every failure, across all endpoints. The key is a stable, machine-readable code that clients can branch on — the human message can be reworded freely, but the code is a promise. Never return a raw stack trace or a bare string; clients can't react to prose, and traces leak your internals.
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "INVALID_QUANTITY", # stable; clients switch on this
"message": "quantity must be >= 1", # human-friendly, may change
"field": "quantity" # which input was wrong
}
}
Authentication: who's calling?
Because REST is stateless, every request must prove its own identity — the server won't remember you from last time. There are three common ways, from simplest to richest:
- API key — a long secret string the caller sends on every request (often
Authorization: Bearer sk_live_...or anX-Api-Keyheader). Simple, great for server-to-server. Downside: one flat secret, easy to leak, all-or-nothing power, awkward to rotate. - OAuth 2.0 — a protocol for "let app B act on my behalf without handing over my password" (the "Log in with Google" flow). The user approves, and the app receives a scoped, expiring access token. OAuth answers "how do I safely get a token"; it usually hands you a JWT.
- JWT (JSON Web Token) — a token format: a base64 string holding claims (e.g.
{"sub":"42","exp":...}) plus a cryptographic signature. The server verifies the signature and trusts the contents without a database lookup — perfect for stateless auth. It says nothing about how you obtained it (that's OAuth's job).
One-line distinction to say out loud: API key = a shared secret; OAuth = the handshake that issues a token; JWT = the shape of the token that handshake hands back.
REST vs GraphQL vs gRPC
REST is the default, but interviewers love the comparison. All three are ways to call a remote server; they differ in who decides the response shape and how it travels.
- REST — many URL endpoints, one per resource; the server decides each response shape. Plays perfectly with HTTP caching, proxies, and browsers. Weakness: a screen needing several resources makes several round-trips, and a fixed shape can over-fetch (send fields you don't need) or under-fetch (force a second call).
- GraphQL — a single endpoint where the client writes a query describing exactly the fields it wants, and the server returns precisely that. Kills over/under-fetching and round-trips. Cost: HTTP caching no longer works for free, and you must guard against expensive client queries (depth/cost limits).
- gRPC — a high-performance scheme using protocol buffers (a compact binary format defined in a
.protoschema) over HTTP/2. Fast, strongly-typed, supports streaming — ideal for service-to-service calls inside a backend. Weakness: not browser-friendly without a proxy, and binary payloads are harder to eyeball.
Rule of thumb for the room: REST for public/web-facing CRUD, GraphQL when many clients need wildly different slices of the same data graph, gRPC for chatty internal microservices where speed and typing matter.
Design principles & pitfalls
A handful of principles separate an API that ages well from one that becomes a support burden:
- Be consistent. One naming convention (plural nouns, snake_case or camelCase — pick one), one date format (ISO-8601 UTC), one error envelope, one pagination style. Surprises are bugs.
- Nouns in URLs, not verbs.
POST /orders, never/createOrder. The HTTP method is the verb. - Use the right status code. Don't return
200with{"error":...}in the body — callers and proxies trust the code. Map failures to the correct 4xx/5xx. - Make writes idempotent where you can. PUT/DELETE naturally; for POST, accept an idempotency key so retries are safe.
Pitfall — chatty APIs. If a single screen has to call /users/42, then /users/42/orders, then /orders/9001/items one by one, the latency stacks up (the "N+1 calls" problem). Fix it by letting a list endpoint embed or expand related data (?expand=orders) or by offering a coarser endpoint — don't make the client orchestrate ten tiny calls.
Pitfall — breaking changes. Renaming a field, removing one, tightening validation, or changing a status code can silently break every existing client. Treat the published contract as frozen: only add optional fields within a version, and ship anything incompatible behind a new /v2/ while keeping /v1/ alive until callers migrate.
Client sends Idempotency-Key: uuid on POST. Server stores the result for ~24h and returns the same response on retry. Required for payments and any "create" that retries can double-execute.
Cursor-based beats offset for large/changing datasets. ?after=cursor&limit=50. Offset breaks when items are inserted/deleted between pages.
URL (/v1/...) is the dumb-and-clear default. Header-based is "cleaner" but harder to debug. Don't break v1; add v2.
Return structured JSON: {"code":"INVALID_EMAIL","message":"...","field":"email"}. Don't leak stack traces. Same shape for every error.
?status=open&sort=-created_at. Document allowed fields. Reject unknown params or you build a forever-compat surface.
REST: cacheable, simple, n endpoints. GraphQL: client picks shape, fewer round-trips, but harder to cache + auth + cost-bound. Use REST unless clients have wildly varying needs.
Takeaway: model the nouns (resources) first, act on them with the standard verbs, and answer with the honest status code. Layer on the four grown-up concerns — idempotency keys for safe retries, cursor pagination for big lists, versioning so you never break callers, and one error shape everywhere. Carry identity on every request (stateless auth via API key / OAuth-issued JWT), and reach past REST to GraphQL or gRPC only when the client's needs genuinely demand it.
Go deeper (optional): the canonical reference for status-code semantics is the HTTP spec (RFC 9110); for token-based auth, read the OAuth 2.0 (RFC 6749) and JWT (RFC 7519) RFCs. Everything you need for an interview is above — these are only for when you want the exact letter of the law.