Chapter 11 — API Design (REST vs gRPC vs GraphQL)
Chapter 11 — API Design (REST vs gRPC vs GraphQL)
Hey everyone! Welcome back to Namaste System Design! 🙏
An API is the menu of a service — it defines what you can order and how. When components talk to each other, they talk through APIs. Get the API design right and everything downstream is pleasant; get it wrong and every client suffers. Today we compare the big communication styles — REST, GraphQL, gRPC, and WebSockets — and then nail down the details that make any API good.
What we will cover:
- What an API contract is (the menu analogy)
- REST — the web's default
- GraphQL — ask for exactly what you need
- gRPC — blazing-fast service-to-service
- WebSockets — when the server needs to talk first
- HTTP verbs, status codes, idempotency, pagination, versioning — worked examples
- Interview Questions
1. The Menu Analogy
┌─────────────────────────────────────────────────────────────┐ │ An API is a CONTRACT — a menu that says: │ │ "Here's what you can ask for, and here's the format │ │ of what you'll get back." │ │ │ │ The client (customer) orders; the server (kitchen) │ │ fulfills. Neither needs to know the other's internals. │ └─────────────────────────────────────────────────────────────┘
2. Communication Styles — Pick Your Protocol
Two services need to talk. There are four common ways to wire that up. Let's go through them one by one.
2.1 REST — the web's default
REST models everything as resources (nouns) accessed via HTTP verbs. It's simple, universal, and cache-friendly.
GET /users/1 → fetch user 1 POST /users → create a user PUT /users/1 → update user 1 DELETE /users/1 → delete user 1 HTTP verbs = actions. URLs = resources. JSON = data.
Request: GET /users/1
Response: 200 OK
{
"id": 1,
"name": "Aisha",
"email": "aisha@example.com",
"address": "...",
"orders": [ ...47 items... ]
}
You only wanted the NAME. You got everything. 📦📦📦
→ OVER-FETCHING.
Now you want the user AND their posts AND their comments:
GET /users/1 → GET /users/1/posts → GET /posts/9/comments
→ 3 round trips for one screen. → UNDER-FETCHING.
✅ PROS Simple. Cacheable (browsers/CDNs cache GET by URL).
Universal — every language/tool speaks HTTP.
❌ CONS Fixed response shape → over/under-fetching.
Many endpoints to design and version.
👉 USE IT WHEN Public APIs, CRUD-style apps, anything that
benefits from HTTP caching.
2.2 GraphQL — ask for exactly what you need
GraphQL (from Facebook) fixes over/under-fetching. There's one endpoint, and the client sends a query describing precisely the fields it wants.
Query: Response (exactly that shape):
{ {
user(id: 1) { "user": {
name "name": "Aisha",
posts { title } "posts": [{ "title": "..." }]
} }
} }
ONE request. EXACTLY the fields asked for. No waste. ✅
✅ PROS No over/under-fetching. One round trip for nested data.
Great for mobile (bandwidth is precious).
❌ CONS Usually POST to one URL → hard to cache with HTTP/CDN.
A badly-written nested query can be very expensive
for the server to resolve.
👉 USE IT WHEN Complex, flexible frontends that need nested,
shaped data — dashboards, mobile apps.
2.3 gRPC — speed between services
gRPC (from Google) is built for service-to-service communication inside your backend, where speed matters more than human-readability.
.proto contract:
service UserService {
rpc GetUser (UserRequest) returns (UserReply);
}
Call: GetUser({ id: 1 })
Reply: UserReply { name: "Aisha", email: "..." }
Sent as BINARY (Protobuf), not JSON text. Tiny & fast. ⚡
✅ PROS Binary Protobuf = small payloads, fast to parse.
Runs on HTTP/2 → multiplexing, streaming both ways.
Strongly typed contract (the .proto file) shared
by client and server — fewer "field renamed" bugs.
❌ CONS Not browser-friendly out of the box.
Binary payloads are hard to eyeball while debugging.
👉 USE IT WHEN Microservices talking to each other inside
your datacenter, where every millisecond and
byte counts.
Microservice A ──gRPC (binary, fast)──▶ Microservice B
Browser ──REST/GraphQL (JSON)──▶ Public API
Rule of thumb: gRPC INSIDE the datacenter,
REST/GraphQL at the PUBLIC edge.
2.4 WebSockets — when the server needs to talk first
REST, GraphQL and gRPC are all request → response: the client always speaks first. But a chat app or a live score tracker needs the server to push data the instant something happens. That needs a persistent, two-way connection: a WebSocket.
Client Server
│──── HTTP handshake ─────────▶│ (upgrade to WebSocket)
│◀─── 101 Switching Protocols ─│
│ │
│◀──── "new message: hi!" ─────│ ← server pushes, unprompted
│───── "got it 👍" ────────────▶│ ← client can push back too
│ │
▼ connection stays OPEN for the whole session ▼
✅ GREAT FOR Chat apps, live notifications, multiplayer games,
live dashboards, collaborative editing (Google Docs)
❌ CONS Stateful connection — the server must hold it open,
which doesn't play as nicely with simple load
balancing (Chapter 04) or serverless scaling.
👉 USE IT WHEN The server needs to push data without the
client asking first, and low latency matters.
Visual Comparison
| REST | GraphQL | gRPC | WebSockets | |
|---|---|---|---|---|
| Data format | JSON (text) | JSON (text) | Protobuf (binary) | Any (often JSON) |
| Direction | Client asks first | Client asks first | Client asks first | ✅ Either side, anytime |
| Fetch shape | Fixed (over/under) | ✅ Exactly what you ask | Fixed, typed | Streamed messages |
| Cache-friendly | ✅ | ❌ | ❌ | ❌ |
| Speed | Good | Good | ✅ Fastest | ✅ Fast (persistent) |
| Best for | Public APIs, CRUD | Complex/flexible frontends | Internal microservices | Real-time, two-way |
What Do Big Companies Actually Use?
Almost always a mix, not one style for everything.
GitHub, Stripe, Twitter/X REST (+ some GraphQL) at the
public edge for third-party devs
Facebook/Meta Built GraphQL — used heavily
for their own mobile/web clients
Netflix, Uber, Google gRPC between internal
microservices; public APIs
still REST/GraphQL at the edge
Slack, WhatsApp, Discord WebSockets for live messages,
REST for everything else
(login, history, settings)
Interview-safe default: "I'd start with REST for a public API because it's simple and cache-friendly; use GraphQL if clients need flexible, nested data; gRPC for high-performance internal service-to-service calls; and WebSockets when the server needs to push data in real time."
3. Good API Design — The Details That Matter
Whatever style you pick, these details separate a good API from a painful one.
3.1 HTTP Verbs — one verb, one meaning
Each verb has a job. Don't invent your own — use the ones HTTP already gives you.
GET /orders/9 → read order 9 (never changes data) POST /orders → create a new order PUT /orders/9 → replace order 9 entirely PATCH /orders/9 → update PART of order 9 (e.g. status) DELETE /orders/9 → remove order 9 ❌ BAD: POST /getOrder (verb in the URL — redundant) ✅ GOOD: GET /orders/9 (URL is the noun, GET is the verb)
3.2 Status Codes — tell the client what actually happened
The status code is the first thing any client checks. Get it right and clients can react automatically — retry, show an error, redirect.
200 OK → success, here's your data 201 Created → success, and I made a new resource 204 No Content → success, nothing to send back 400 Bad Request → you sent something malformed 401 Unauthorized → you're not logged in 403 Forbidden → you're logged in, but not allowed 404 Not Found → that resource doesn't exist 429 Too Many Requests → slow down (Chapter 13) 500 Internal Error → the server broke, not your fault
👉 Use it when: always — a REST API that returns 200 OK with { "error": "not found" } in the body forces every client to parse the body just to know if it worked. Let the status code do that job.
3.3 Idempotency — safe to retry
A request is idempotent if doing it once or five times leaves the system in the same state. This matters because networks fail and clients retry.
GET, PUT, DELETE → idempotent by design
(deleting order 9 twice = order 9 is still gone. Fine.)
POST → NOT idempotent by default
┌─────────────────────────────────────────────────────┐
│ Client → POST /orders (charge ₹500) → times out │
│ Client doesn't know if it worked → RETRIES │
│ Server actually got both → charges ₹500 TWICE 💥 │
└─────────────────────────────────────────────────────┘
FIX: client sends an Idempotency-Key header, a unique
ID it generates once per logical action.
POST /orders Idempotency-Key: abc-123
→ server remembers abc-123 already ran → returns the
SAME result on retry, without charging again. ✅
3.4 Pagination — never return a million rows
A list endpoint that returns everything will eventually return too much. Page it.
OFFSET pagination:
GET /orders?limit=20&offset=40 → rows 41-60
❌ Gets slow on huge tables (DB still scans past 40 rows)
❌ Items can shift if rows are inserted mid-scroll
CURSOR pagination:
GET /orders?limit=20&after=order_60
✅ "Give me 20 after the one I last saw" — fast, and
stable even while data changes.
This is what Twitter/X, Slack and Stripe use.
3.5 Versioning — change without breaking old clients
/v1/users ← existing clients keep working /v2/users ← new shape, new clients opt in Old mobile app on v1 keeps working after you ship v2. Once nobody calls v1 anymore, you retire it.
👉 Use it when: you need to change a field's type, remove a field, or restructure a response — anything that would break a client parsing the old shape.
3.6 Consistency & Security — the rest of the checklist
✔ Same naming everywhere: snake_case OR camelCase, not both
✔ Same error shape everywhere: { "error": { "code", "message" } }
✔ Auth on every endpoint that needs it (tokens, OAuth)
✔ Rate limiting to stop abuse (Chapter 13)
Interview Questions — Quick Fire!
Q: What is REST?
"REST is an architectural style where everything is modeled as a resource identified by a URL, and you act on it using HTTP verbs — GET to read, POST to create, PUT to update, DELETE to remove — typically exchanging JSON. It's simple, stateless, and cache-friendly, which is why it's the default for public web APIs."
Q: What problem does GraphQL solve?
"It solves over-fetching and under-fetching. With REST, an endpoint returns a fixed shape, so you often get more data than you need or have to make several calls to assemble what you want. GraphQL exposes one endpoint where the client specifies exactly which fields it needs, so it gets precisely that in a single request — great for complex frontends and bandwidth-limited mobile clients."
Q: When would you use gRPC over REST?
"For internal service-to-service communication where performance matters. gRPC uses binary Protobuf over HTTP/2, which is much faster and smaller than JSON, supports streaming, and enforces a strongly typed contract. The downside is it's not browser-friendly and harder to debug by eye, so I'd use gRPC inside the datacenter between microservices and REST or GraphQL at the public edge."
Q: When would you reach for WebSockets instead of REST?
"When the server needs to push data to the client without the client asking first — chat messages, live notifications, multiplayer game state. REST is always request-then-response; a WebSocket keeps a connection open so either side can send at any time, which is what real-time features need."
Q: What does idempotent mean, and why does it matter for APIs?
"An idempotent request produces the same end state whether it's called once or many times. GET, PUT, and DELETE are naturally idempotent; POST isn't, which is dangerous because clients retry on timeouts — a retried POST could charge a customer twice. The fix is an idempotency key: the client sends a unique ID with the request, and the server recognizes a repeat and returns the original result instead of repeating the side effect."
Q: Why is API versioning important?
"Because clients depend on the API's shape. If you change it without versioning, you break every existing client. Versioning — like putting /v1/ in the path — lets you evolve the API by releasing /v2/ while old clients keep using /v1/ until they migrate, avoiding breaking changes."
Q: How would you paginate a large list endpoint?
"Never return the whole table. Offset pagination — limit and offset — is simple but gets slow on large tables and can skip or repeat rows if data changes mid-scroll. Cursor pagination — give me N items after the last one I saw — is faster and stable under concurrent writes, which is why most large-scale APIs like Stripe and Twitter use it."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| REST | Resources + HTTP verbs + JSON. Simple, cache-friendly; suffers over/under-fetching. |
| GraphQL | One endpoint, client asks for exact fields. Flexible; harder to cache. |
| gRPC | Binary Protobuf over HTTP/2. Fastest; best for internal microservices. |
| WebSockets | Persistent two-way connection. Server can push without being asked first. |
| Status codes | Let the code carry meaning: 2xx success, 4xx client error, 5xx server error. |
| Idempotency | Safe to retry. Use an Idempotency-Key for POSTs that have side effects. |
| Pagination | Cursor-based scales better than offset-based for large, changing datasets. |
| Versioning | /v1/, /v2/ — evolve the API without breaking clients still on the old version. |
What's Next?
Chapter 12 covers how we serve content fast to the whole planet: the CDN. How does a user in Tokyo get a video as quickly as a user in New York? By keeping copies close to everyone.
Keep designing, keep scaling! See you in the next one!
Post a Comment