After you gather requirements, the next concrete artifact in a system design interview is usually the API — the set of operations clients can perform. A good API sketch anchors the whole design: it forces you to name the core entities, decide what’s read-heavy vs write-heavy, and expose exactly the access patterns the system must support. Fumbling the API signals shallow thinking; a clean one buys you credibility for the rest of the interview.
Three styles, three jobs
REST
REST models everything as resources addressed by URLs, acted on with HTTP verbs:
POST /v1/posts → create a post
GET /v1/posts/{id} → fetch one
GET /v1/users/{id}/posts → list a user's posts
DELETE /v1/posts/{id} → deleteThe verbs carry meaning (GET is safe and cacheable, PUT/DELETE are idempotent), status codes communicate outcomes (200, 201, 404, 409, 429), and each request is stateless — the server keeps no per-client session, so any instance can handle any request. REST is the right default for public APIs: it’s cacheable, universally understood, and easy to debug. Its weakness is over- and under-fetching — a fixed response shape often returns more than a screen needs, or too little, forcing extra round trips.
gRPC
gRPC uses Protocol Buffers over HTTP/2 — a compact binary format with a schema-defined contract. It’s fast, strongly typed, and supports streaming. It shines between internal services where both ends are yours and you want speed and generated client code. It’s a poor fit for browsers (needs a proxy) and harder to inspect by hand. (See the Networking lesson for the protocol details.)
GraphQL
GraphQL exposes a single endpoint where the client specifies exactly the fields it wants:
query {
user(id: "42") {
name
posts(last: 10) { title, likeCount }
}
}This eliminates over- and under-fetching and collapses many REST calls into one — ideal for rich, varied clients (a mobile app and a web app with different needs). The costs: HTTP caching is harder (everything is a POST to one URL), a naive resolver can trigger the N+1 query problem, and a malicious query can be expensive, so you need depth/complexity limits.
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Best for | Public, cacheable APIs | Internal service-to-service | Flexible client-driven reads |
| Payload | JSON (text) | Protobuf (binary) | JSON |
| Fetching | Fixed shapes (over/under-fetch) | Fixed, typed | Client picks fields |
| Caching | Easy (HTTP) | Manual | Harder |
| Interview default | Yes, unless a reason not to | Internal hops | Many clients, varied needs |
Pagination: cursor, not offset
Any endpoint returning a list needs pagination, and the choice is a classic senior signal.
- Offset pagination (
LIMIT 20 OFFSET 40) is simple but has two problems at scale: the database still scans and discards all the skipped rows (slow on deep pages), and if items are inserted or deleted between requests, pages drift — the user sees duplicates or misses rows. - Cursor pagination passes an opaque pointer to the last item seen (
?after=<cursor>), and the query becomesWHERE id > :last ORDER BY id LIMIT 20. It’s stable under inserts, fast at any depth (it uses the index instead of scanning), and is the production default for feeds and infinite scroll.
GET /v1/posts?after=eyJpZCI6MTIzfQ&limit=20
→ { "items": [...], "next_cursor": "eyJpZCI6MTQzfQ" }Idempotency: make retries safe
Networks fail. A client sends POST /payments, the response is lost, and the client retries — do you charge twice? The fix is an idempotency key: the client generates a unique key per logical operation and sends it with the request. The server records the key and its result; a repeat with the same key returns the original result instead of acting again.
POST /v1/payments
Idempotency-Key: 9f1c-4b2a-...
→ first call: charge, store result under key
→ retry with same key: return stored result, do NOT re-chargeGET, PUT, and DELETE are naturally idempotent; POST is the one that needs this treatment. Calling it out unprompted on any create/charge endpoint is a strong signal.
Versioning and evolution
APIs are contracts, and contracts change. Version from the start:
- URL versioning (
/v1/,/v2/) — explicit and easy to route; the most common choice. - Header versioning (
Accept: application/vnd.api.v2+json) — cleaner URLs, less discoverable.
Prefer additive, backward-compatible changes (new optional fields) over breaking ones. Reserve a new major version for changes that would break existing clients, and keep the old version running during a deprecation window.
Cross-cutting concerns live at the edge
Authentication, rate limiting, and request validation belong at the API Gateway, not in every service (see Common Components). A consistent error format helps too:
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests", "retry_after": 30 } }Return standard status codes (429 with Retry-After for throttling, 409 for conflicts, 422 for validation) so clients can react programmatically.
Design for the access pattern
The best APIs are shaped by the queries the system must answer, not by a tidy ER diagram. If the product needs “a user’s 20 most recent posts,” design GET /users/{id}/posts?limit=20 and make sure the data model indexes it cheaply. Working backward from the access pattern keeps you from exposing endpoints the storage layer can’t serve efficiently — and it sets up the data modeling discussion that follows.
In the interview
- Write the API early, right after requirements — it crystallizes the design.
- State the style and why: “REST for the public API because it’s cacheable; gRPC between our services for speed and typed contracts.”
- Call out cursor pagination and idempotency keys proactively on list and write endpoints — few candidates do, and it reads as production experience.
- Keep it small: 4–6 endpoints that cover the core flows beat an exhaustive list.
Key Takeaways
- Match the style to the caller. REST for public/cacheable, gRPC for internal service-to-service, GraphQL for flexible client-driven reads.
- Use cursor pagination. It’s stable under writes and fast at any depth; offset drifts and slows down.
- Make writes idempotent. An idempotency key turns a dangerous retry into a safe no-op — essential for payments and any create.
- Version from day one and prefer additive, backward-compatible changes.
- Design around the access pattern. The API should expose exactly the queries the product needs, served cheaply by the data model.
