Fundamentals

API Design

REST vs. RPC vs. GraphQL, idempotency keys, cursor pagination, and versioning without breaking existing clients.

12 minMediumapi-designfundamentals

The API is the one part of the design everyone in the room can actually read without a diagram. Interviewers pay attention to it disproportionately for exactly that reason — a sloppy endpoint shape is visible to everyone, not just to whoever's deep-diving the database.

REST vs. RPC vs. GraphQL

RESTRPCGraphQL
ShapeResources + HTTP verbsNamed function callsOne endpoint, client-specified query
Good fitCRUD-shaped domainsInternal service-to-service callsMany clients with different data needs
CostVerb/resource mismatches get awkward (POST /users/123/deactivate)No shared convention for "what does 404 mean here"A slow nested query can hide N+1 database calls behind one request

None of these is universally correct. A reasonable default: REST for a public-facing CRUD API, RPC for internal service calls where you control both sides, GraphQL only when you actually have several client types with genuinely different data needs — reaching for it by default adds a query planner's worth of complexity you may not need.

Idempotency

A POST /payments call that times out on the client side, then gets retried, must not charge the customer twice. The standard fix is an idempotency key: the client generates one per logical operation and sends it in a header; the server stores which keys it has already processed and returns the original result on a repeat, instead of re-executing.

Client

POST /payments, Idempotency-Key: abc123

API layer

checks abc123 against processed keys

Not seen before

processes charge, stores result under abc123

Client retries (timeout)

same key abc123

API layer

returns the stored result — no second charge

Pagination: cursor vs. offset

GET /posts?offset=100000&limit=20 looks harmless until the table has tens of millions of rows — the database still has to scan and discard the first 100,000 rows before it can return the next 20. A cursor (GET /posts?after=post_98213&limit=20) carries a pointer to the last item seen instead of a row count, so the query becomes "give me 20 rows after this one" — a lookup, not a scan. The cost: a cursor can't jump to "page 47" directly, which offset-based pagination can. Most feeds and infinite-scroll UIs don't actually need that, which is why cursor pagination is the more common real-world choice at scale.

Versioning

Public APIs change, and existing clients can't be forced to update on your schedule. Two common approaches: a version segment in the URL (/v2/orders) or a version header (Stripe-Version: 2024-06-20, the pattern Stripe's public API actually uses). URL versioning is more visible and cacheable; header versioning keeps URLs stable and is easier to default silently for clients that don't specify one. Either is defensible — what's not defensible in an interview is not having thought about it at all, since "how do you change this without breaking existing clients" is a near-certain follow-up on any public-facing API design.