◐ system-design/api-design.md
21. API Design: REST, GraphQL, gRPC
APIs are the contracts between systems. Designed well, they outlive their authors. Designed poorly, they outlive them too — painfully.
~6 min read·updated 5/29/2026
21. API Design: REST, GraphQL, gRPC
APIs are the contracts between systems. Designed well, they outlive their authors. Designed poorly, they outlive them too — painfully.
21.1 REST
Roy Fielding (2000). Style, not protocol. Constraints: client-server, stateless, cacheable, uniform interface, layered.
Resource-oriented
- URLs identify resources, not actions:
/users/123/ordersnot/getOrdersForUser?id=123. - HTTP verbs are the actions:
GET,POST,PUT,PATCH,DELETE. - Status codes carry meaning (200, 201, 404, 409, 422, 429, 500).
Idempotency
GET,HEAD,PUT,DELETEare idempotent. Safe to retry.POST,PATCHare not — useIdempotency-Keyheader (Stripe convention).
HATEOAS
"Hypermedia as the Engine of Application State." Responses include links to next actions. Theoretically pure REST. In practice, almost no one does it. Most "REST APIs" are really RPC over HTTP with JSON.
Versioning
/v1/users(URL): simple, easy to deprecate, clear in logs.Accept-Version: 2024-10-01(header): cleaner URLs.- Date-based (Stripe): backwards-compatible-by-default; clients pin date.
Pagination
- Offset/limit (
?page=2&size=20): simple; performance dies on deep offsets (DB scans skipped rows). - Cursor-based (
?cursor=eyJpZCI6MTIzfQ): pass an opaque cursor, often encoded(sort_field, id). Stable across writes; required at scale. - Keyset (
?after_id=123): special case of cursor; very simple; assumes stable sort key.
Always cursor-based for any list that can grow.
Filtering, sorting, fielding
?status=active&sort=-created_at&fields=id,name- Avoid Cartesian-product query languages; you'll regret them.
- Strict whitelisting of allowed sorts/filters.
Bulk vs single
- 1000 single requests: high overhead.
- 1 bulk request: efficient but failure semantics are tricky (partial success?).
- Patterns:
POST /users/batchreturns array of{id, status}; clients retry the failed ones.
Errors
Use standard HTTP status + structured error body:
{ "error": { "code": "user_not_found", "message": "User 123 does not exist", "request_id": "abc" } }
RFC 7807 (Problem Details for HTTP APIs) standardizes this.
Long-running ops
For ops that don't return immediately:
202 Acceptedwith a status URL.- Client polls
/operations/{id}for status. - Or webhook callback when done.
21.2 GraphQL
Facebook (2015). Single endpoint, query language, client picks fields.
Pros
- Avoids over-fetching: client gets exactly what it asks for. Mobile clients love this.
- One round trip for nested data.
- Strong typing via schema (SDL).
- Introspection: tooling (GraphiQL) built in.
- Versionless: deprecate fields rather than versioning endpoints.
Cons
- Caching is harder: every query is unique; no URL-based HTTP cache. Solutions: persisted queries (server pre-registers a query, client sends only an ID).
- N+1 queries: naive resolvers query DB per nested field. Solution: DataLoader (batch + cache within a request).
- Server load harder to predict: clients can craft expensive queries. Solutions: query depth limit, complexity scoring, query cost analysis, persisted queries.
- Authorization is per-field: more code surface to get right.
- Subscriptions: real-time via WebSockets; operationally heavier.
When it shines
- Mobile clients with constrained bandwidth.
- Many client types reusing one backend.
- Aggregating data from many services (federated GraphQL: Apollo Federation).
When to skip
- Server-to-server APIs (use gRPC).
- Simple CRUD (REST is less effort).
- Caching is critical (REST + HTTP cache wins).
21.3 gRPC
Google's RPC framework. Protobuf + HTTP/2.
Pros
- Compact wire format (Protobuf).
- Strong typing, codegen for many languages.
- Streaming: client, server, or bidirectional.
- HTTP/2 multiplexing: many concurrent calls per connection.
- Built-in deadlines, retries, cancellation, metadata.
Cons
- Browser support poor: needs gRPC-Web or REST shim.
- Less human-friendly: harder to debug with curl.
- Schema management: must distribute .proto files.
When to use
- Internal service-to-service.
- High throughput / low latency required.
- Polyglot codebase wanting one schema source.
Streaming patterns
- Server-streaming: client sends one request; server streams responses (price ticks, log tail).
- Client-streaming: client streams; server replies once (file upload, batch insert).
- Bidirectional: full duplex (chat, real-time collab).
21.4 REST vs GraphQL vs gRPC
| Need | Pick |
|---|---|
| Public API for many clients | REST (familiar, cacheable) |
| Mobile/web with one team's flexible needs | GraphQL |
| Internal services, high throughput | gRPC |
| Real-time bidirectional | gRPC streaming or WebSocket |
| LLM tool-call APIs | REST + JSON |
Most production systems use a mix: REST at the edge, gRPC internally, GraphQL for specific BFFs.
21.5 API design principles
Consistency
- Naming:
created_ateverywhere, notcreatedAthere andcreated_datethere. - Pagination: same shape across endpoints.
- Errors: same envelope.
- Pluralization:
/users/123not/user/123.
Predictability
- Same verb same effect.
- Resource paths describe hierarchy:
/users/{id}/orders/{order_id}/items.
Backwards compatibility
- Never remove or rename fields.
- Add optional fields freely.
- Add new endpoints; deprecate old with timelines.
Pagination, filtering, sorting
- Implement once, consistently.
Errors with correlation
- Always return a
request_id(ortrace_id) so support can find your logs.
Rate limit headers
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset.Retry-Afteron 429 / 503.
Idempotency
- Document which endpoints are idempotent.
- Provide
Idempotency-Keyfor non-idempotent ops.
21.6 Auth
(See chapter 27.)
- API key: simple, server-to-server. Risk if leaked.
- OAuth 2.0: delegated auth. Three-legged for end users; client credentials for server-to-server.
- JWT: stateless tokens with claims. Verifiable without a DB lookup.
- mTLS: client certs (zero-trust networks).
Always TLS. Always verify the audience/issuer of JWTs.
21.7 Webhooks
Server pushes events to client URLs.
Concerns
- Reliability: retries with exponential backoff; dead-letter after N attempts.
- Idempotency: receivers must dedupe (event ID).
- Authentication: HMAC-sign payloads; receiver verifies signature using shared secret.
- Replay protection: include timestamp in signature; receiver rejects old payloads.
Stripe, GitHub, Slack — all do this well. Steal their patterns.
21.8 Server-Sent Events (SSE) and WebSockets
For real-time push (chapter 3):
- SSE: simple, server-to-client only, over HTTP.
- WebSockets: bidirectional, more complex.
- gRPC streaming: bidirectional, internal.
21.9 Long polling vs polling
(Chapter 3 again.) Polling is simple but wasteful. Long polling = server holds the request until an event happens or timeout. WebSocket beats long polling at scale; long polling beats WebSocket for compatibility.
21.10 API governance
In big organizations:
- Style guide (Google's API design guide is excellent and public).
- Linting (Spectral for OpenAPI).
- Contract testing (Pact, consumer-driven).
- API catalog / discovery (Backstage, internal portals).
- Deprecation policy with timelines.
- Change advisory across teams.
21.11 OpenAPI / Swagger
Schema-first REST. Define endpoints, schemas, examples in YAML/JSON. Generate client SDKs, server stubs, docs.
Used by: Stripe (OpenAPI generated docs), Twilio, GitHub.
For gRPC: the .proto is the schema. For GraphQL: the SDL is the schema.
Schema-first lets you build clients before the server is done; lets the server enforce; makes the contract reviewable.
21.12 Common API mistakes
- No pagination, list endpoint returns 50K items.
- No idempotency on POSTs that have side effects.
- Inconsistent naming across endpoints.
- Returning 200 with
{ error: ... }in the body instead of the right HTTP code. - No versioning plan — first breaking change becomes existential.
- Leaking internal IDs (PostgreSQL
bigserial) instead of opaque IDs (UUID/ULID). - No rate limiting until you're DDoSed.
- Sync API for long-running operation times out.
21.13 What to demonstrate in an interview
- Pick the right protocol with reasoning (REST vs gRPC vs GraphQL).
- Cursor pagination, idempotency, rate limit headers, versioning.
- Auth model (JWT vs OAuth vs API key).
- Error envelope.
- Specific Stripe/GitHub/Twilio patterns: they've solved most problems already.
Key takeaways
- REST = default for public APIs. Cursor pagination, idempotency keys, structured errors.
- GraphQL for mobile/web with diverse data needs; watch caching and N+1.
- gRPC for internal RPC; streaming included; Protobuf-typed.
- Versioning plan from day one; backwards compatibility forever.
- Webhooks: HMAC-signed, retried, idempotent.
// 3 views