What it is
The contract between a service and its callers. Good API design decides six things: protocol (REST vs gRPC vs others), resource modeling, versioning, pagination, idempotency, and how identity and context flow through requests.
When you care
Every system design interview has an API section. Candidates who name three endpoints and move on lose signal to candidates who call out idempotency keys, pagination style, and versioning strategy in the same breath. The cheap wins live here.
REST vs gRPC
| Dimension | REST | gRPC |
|---|---|---|
| Wire format | JSON over HTTP/1.1 or HTTP/2 | Protobuf over HTTP/2 |
| Schema | OpenAPI (optional) | .proto file (required) |
| Codegen | Optional | Native, multi-language |
| Browser support | Native | Requires gRPC-Web proxy |
| Streaming | SSE or WebSocket as sidecar | Built-in (client, server, bidi) |
| Human-readable | Yes | No (binary) |
| Versioning | URL or header-based | Proto field numbers |
| Good for | Public APIs, browser clients, third-party integrations | Internal service-to-service, polyglot backends, streaming |
REST is the default for anything a browser or third party will call. gRPC is the default for internal microservice traffic where both sides are under your control. Most production systems use both — REST at the edge, gRPC inside.
RESTful resource modeling
- Nouns, not verbs.
POST /orders, notPOST /createOrder. - Plural resource names.
/users/123, not/user/123. - Hierarchy reflects ownership.
/users/123/ordersfor user-scoped resources; top-level/ordersfor globally addressable ones. - Status codes carry meaning.
201 Createdon successful POST,204 No Contenton DELETE,409 Conflicton idempotency mismatch,422 Unprocessable Entityon validation failure.
Identity from context, not parameters
Never accept user_id as a request parameter on endpoints that act on
the caller. Derive it from the authenticated session.
BAD: GET /api/orders?user_id=123
GOOD: GET /api/orders (user_id derived from auth token)
user_id as a parameter invites authorization bugs — someone changes
123 to 124 and sees another user’s orders. Sensitive identifiers
come from the auth context, not the request body. The parameter-based
form is only acceptable when the caller legitimately acts on behalf of
another user (admin endpoints, support tools) and that authorization is
explicitly checked.
Versioning
| Strategy | Form | Tradeoff |
|---|---|---|
| URL versioning | /v1/orders, /v2/orders | Explicit, visible, easy to route. Breaks link permanence across versions. |
| Header versioning | Accept: application/vnd.api+json; version=2 | URLs stay stable. Harder to debug; invisible in logs. |
| Parameter versioning | /orders?version=2 | Rarely used; mixes versioning with query semantics. |
| Never | Always breaking | Don’t. |
URL versioning is the default for public APIs. Bump major versions for breaking changes; add optional fields for non-breaking additions. gRPC uses proto field numbers for the same purpose — adding new fields is backward-compatible by design.
Pagination
| Style | Request | Response | Good for |
|---|---|---|---|
| Offset / limit | ?offset=100&limit=20 | Items + total count | Small datasets, random-access UIs (page 47). |
| Cursor-based | ?cursor=abc123&limit=20 | Items + next_cursor | Large datasets, feeds, infinite scroll. |
| Keyset (seek) | ?after_id=999&limit=20 | Items (client uses last ID) | Monotonically ordered data; highest performance. |
Offset pagination is simple but breaks at scale. OFFSET 1,000,000
on a DB is an O(N) scan. It also returns inconsistent results when the
underlying data changes mid-pagination.
Cursor pagination encodes the position as an opaque token (often a signed, base64-encoded row ID + sort key). Handles insertions gracefully, scales cleanly, and hides implementation details from the client. The default choice for any feed, list, or log.
Keyset pagination is cursor pagination with a known structure (usually the primary key). Fastest option when the ordering matches the index. Used heavily in internal systems.
Idempotency
An idempotent API produces the same observable result when called once or multiple times with the same input. Idempotency is a correctness property for network retries — if the client retries a payment because a response timed out, you do not want two payments.
| Method | Idempotent? | Notes |
|---|---|---|
| GET, HEAD | Yes | Natural. |
| PUT | Yes | Replaces the resource; same PUT = same state. |
| DELETE | Yes | Second DELETE returns 404 or 204, not an error. |
| POST | Not by default | Make it idempotent via idempotency keys. |
| PATCH | Depends | Only if the patch is a replace, not an increment. |
Idempotency keys. For non-idempotent operations (mostly POST), the
client generates a unique key (UUID) per logical operation and sends it
as a header: Idempotency-Key: abc-123. The server stores a mapping of
key → result for some window (commonly 24 hours). On retry, it returns
the stored result without re-executing.
Every non-idempotent endpoint that matters (payments, order creation, transfers, message sends) should accept an idempotency key. This is a small design decision with a large correctness payoff, and it’s one of the cheapest signals to display in an interview.
Common practices worth naming
- Consistent error shape. One envelope across all errors:
{ error: { code, message, details } }. Clients parse once. - Field naming. Pick
snake_caseorcamelCaseand never mix.snake_caseis the REST/JSON convention in most style guides. - Timestamps are ISO 8601 strings, UTC.
2026-05-08T14:30:00Z. Numeric Unix timestamps invite timezone bugs. - Rate limiting at the edge.
429 Too Many RequestswithRetry-Afterheader. Belongs in a gateway, not per-endpoint logic. - Null vs missing. Decide whether absent fields mean “no value” or “don’t change” (PATCH). Document it.
When to pick what
- Public API, browser clients, third-party integrations: REST with URL versioning, cursor pagination, idempotency keys on all non-GET endpoints.
- Internal microservice traffic: gRPC with protobuf; use REST only at the edge.
- Streaming: gRPC streaming for internal; SSE or WebSocket for browser-facing.
- Operations that modify state: PUT if idempotent by design, POST with idempotency key otherwise.
Related
- Walkthrough: Designing an Ad Click Aggregation System — the
event_idfield is a canonical idempotency-key example. - Walkthrough: Designing a URL Shortener — a minimal REST API with interesting 301 vs 302 tradeoffs.
- Walkthrough: Designing a RAG System — illustrates
202 Acceptedfor async ingestion and tenant context from auth.