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

RESTGraphQLgRPCWebSockets
Data formatJSON (text)JSON (text)Protobuf (binary)Any (often JSON)
DirectionClient asks firstClient asks firstClient asks first✅ Either side, anytime
Fetch shapeFixed (over/under)✅ Exactly what you askFixed, typedStreamed messages
Cache-friendly
SpeedGoodGood✅ Fastest✅ Fast (persistent)
Best forPublic APIs, CRUDComplex/flexible frontendsInternal microservicesReal-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

ConceptKey Takeaway
RESTResources + HTTP verbs + JSON. Simple, cache-friendly; suffers over/under-fetching.
GraphQLOne endpoint, client asks for exact fields. Flexible; harder to cache.
gRPCBinary Protobuf over HTTP/2. Fastest; best for internal microservices.
WebSocketsPersistent two-way connection. Server can push without being asked first.
Status codesLet the code carry meaning: 2xx success, 4xx client error, 5xx server error.
IdempotencySafe to retry. Use an Idempotency-Key for POSTs that have side effects.
PaginationCursor-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!