Study Notes
Noteapplied

API Design — REST, gRPC & GraphQL

How backend services talk to each other: the design rules that keep those conversations safe and fast — HTTP methods, status codes, versioning, idempotency, and pagination — then the three styles you actually choose between, and when each one wins.

35 min read·verified Content review 2026·reviewed 2026-08-30

After this you should be able to

  • Explain the core idea in API Design — REST, gRPC & GraphQL
  • Apply API Design — REST, gRPC & GraphQL in a representative design
REST · gRPC · GraphQLIdempotencyVersioning · Pagination
System Design · API Design

How backend services talk to each other — the design rules that keep those conversations simple, safe, and fast, and the three architectural styles you choose between when you draw the boxes.

Updated August 2026 · Read ~22 min · Scope: session as taught


// the big idea

An API is a contract between two services — it fixes the request shape, the response shape, and the rules of the exchange. Design it well (resource URLs, right verbs, versioning, idempotency) and then pick a style by the job: REST for public and CRUD, gRPC for fast internal microservice chatter, GraphQL when the client must ask for exactly the fields it needs.

1

Notebook Summary#

Hand-copy this. Every line is derivable from the Detailed Notes in §2.

API — the basics#

  • API = Application Programming Interface → lets 2 services talk.
  • Defines: how request sent · request body · data format · how response returned.
  • Flow: client → API request → server → API response → client.
  • Doodle: waiter carrying order slip between diner & kitchen.

Good design principles#

  • Simple + consistent → standard naming. GET /users/123 ✓   GET /getUser?id=123 ✗
  • Resource-based URL → nouns not verbs. /users /orders /products. Verb lives in HTTP method.
  • Scalable → pagination · caching · rate-limiting · load-balancing.
  • Secure → HTTPS · authN (who are you) · authZ (what can you touch) · input validation.
  • Versioned → /v1 /v2 so old clients don't break.
  • Idempotent → same request → same result.

HTTP methods#

VerbDoesIdempotent?
GETreadyes
POSTcreate newno (by default)
PUTreplace whole resourceyes
PATCHpartial updatedepends
DELETEremoveyes

Request parts#

  • Path param → identifies resource → /users/123
  • Query param → filter/sort/paginate → ?page=2&limit=20
  • Headers → auth token, content-type, idempotency-key
  • Body → the data payload (JSON)

Status codes#

ClassMeansExamples
2xxsuccess200 OK · 201 Created · 204 No Content
4xxclient error400 · 401 · 403 · 404 · 429
5xxserver error500 · 503
  • Memory: 2 = "you did fine", 4 = "you messed up", 5 = "I messed up".

Idempotency ⭐#

  • Repeat same request → same final state. Stops double-charge on double-click.
  • Fix POST: client sends idempotency-key (UUID) in header. Server stores result vs key; retry returns stored result, doesn't re-run.
  • ⭐ Natural idempotency by verb: PUT ✓ · DELETE ✓ · GET ✓ · PATCH depends · POST ✗.
  • ⚠ PATCH increaseBalance:100 → NOT idempotent (state grows each call). PATCH name:"John" → idempotent.
  • ⚠ Store the key BEFORE processing, else a failed write lets the retry double-charge.
  • Doodle: two identical "PAY" clicks → one coin drops into till.

Versioning#

  • Ship /v2, keep /v1 live, migrate clients, then retire v1.

Pagination#

  • Return large data in pages, not one dump. ?page=2&limit=20.
  • Feed apps pull more as you scroll (~80% mark) → saves bandwidth on data you'd never read.

REST vs gRPC vs GraphQL ⭐#

RESTgRPCGraphQL
FormatJSONbinary (protobuf)JSON
TransportHTTPHTTP/2HTTP
Speedmediumvery highmedium
Best forpublic API, CRUDinternal microservicesclient picks fields
By—GoogleFacebook
  • REST = stateless, resource-oriented, on HTTP. Human-readable. ~85% of the world.
  • REST pains → over-fetching (too much data) · under-fetching (many calls) · JSON heavier than binary.
  • gRPC solves heavy payload (binary + HTTP/2). GraphQL solves over/under-fetch.
  • ⚠ gRPC needs BOTH sides in gRPC — can't mix with a REST-only third party.
  • Doodle: REST=postcard, gRPC=zipped courier packet, GraphQL=à-la-carte order slip.

2

Detailed Study Notes#

A customer taps Pay, the screen freezes, and they tap it again. Two payments leave their account. Somewhere a backend service received the same intent twice and treated each as a fresh order. Nothing was hacked — the API just had no way to know the second request was the same as the first. That one gap explains why API design is a discipline and not an afterthought: the rules below exist to stop exactly these quiet, expensive mistakes.

2.1What an API actually is#

An API is the agreed way for two pieces of software to talk. In a system design, your product is a set of backend services — boxes on a diagram. None of them is useful alone; they have to exchange information. The interface that governs that exchange is the API (its full name, Application Programming Interface, is worth saying once and then forgetting).

The API pins down the parts of the conversation that both sides must agree on: how a request is sent, what the request body looks like, which data format carries it, and how the response comes back. Get the contract right and the two services never have to meet — they just honour the shape.

Analogy · recall

A restaurant waiter. You (the client) hand over an order; the waiter carries it to the kitchen (the server); the kitchen cooks and the waiter brings back a plate (the response). You never walk into the kitchen. The menu — what you can order and in what form — is the API contract.

Client app Server API request API response
fig 2.1 — the round trip: request out, response back, contract in between

2.2What makes an API well-designed#

Six habits separate an API a stranger can use from one that generates support tickets.

Simple and consistent#

An endpoint should read the way a person expects. The verb belongs to HTTP, and the URL names the thing. GET /users/123 is right; GET /getUser?id=123 is wrong twice over — it repeats the verb (GET already means "get") and buries the identifier in a query parameter when it identifies a specific resource and belongs in the path.

Resource-based URLs#

URLs name nouns, not actions. The action is carried by the HTTP method, so the path is free to describe what you are acting on. Good: /users, /orders, /products. Avoid /getUser or /createOrder — the verb is redundant with GET and POST.

Client→ API Gateway→ Load Balancer→ Backend Service→ Database

Where the API lives — the request path through a typical system-design template.

Scalable, secure, versioned#

Scalable means the API is built to handle growth: pagination for large result sets, caching, rate limiting, and load balancing. Secure means HTTPS on the wire plus two distinct checks — authentication ("who are you?") and authorization ("what are you allowed to touch?") — and input validation so a malformed body is rejected at the door, before it reaches business logic. Versioned means you can evolve without breaking anyone; more on that in §2.5.

💡 TIP · interview

Authentication vs authorization trips people up. AuthN is identity (login, JWT). AuthZ is permission (this admin may delete; this user may only read). A request can be authenticated yet unauthorized — that's the difference between 401 and 403.

2.3The four parts of a request#

Every request the client sends is assembled from four slots, and knowing which slot a value belongs in is half of good API design.

path parameter

Identifies a specific resource. In GET /users/123, the 123 is the user id.

query parameter

Filters, sorts, or paginates. After the ?: /users?page=2&limit=20.

headers

Side information: Authorization: Bearer …, Content-Type: application/json, idempotency keys.

request body

The payload the client sends the server, e.g. { "name": "John", "age": 30 }.

2.4Status codes — the server's one-word reply#

A status code is how the server tells the client, in a number, how the request went. The leading digit is the whole story; the rest is nuance.

CodeClassMeaning
2002xx successOK — request succeeded
2012xx successCreated — a new resource exists
2042xx successNo Content — succeeded, nothing to return
4004xx clientBad Request — invalid input, wrong contract
4014xx clientUnauthorized — authentication missing
4034xx clientForbidden — authenticated but no permission
4044xx clientNot Found — endpoint or resource doesn't exist
4294xx clientToo Many Requests — rate limit hit [+]
5005xx serverInternal Server Error — the server broke
5035xx serverService Unavailable — temporarily down
💡 TIP

Read the first digit like a mood: 2 = "you did fine," 4 = "you sent me something wrong," 5 = "I broke, not your fault." That single digit tells you which side to debug before you read another word.

2.5Versioning — changing without breaking#

Versioning lets an API evolve while existing clients keep working. Suppose clients already call /v1/users. A new business requirement forces the request or response shape to change. If you edit /v1 in place, every current caller that sends the old shape starts failing — a bad experience nobody asked for.

Instead you publish /v2/users alongside /v1, onboard new consumers onto v2, migrate the old ones on their own schedule, and only then retire v1.

Analogy · recall

A new edition of a textbook. You don't overwrite everyone's copy overnight — you print the 2nd edition, let classes switch over a term, and keep the 1st edition usable until the last class moves on.

2.6Idempotency — the double-click insurance#

A request is idempotent if repeating it leaves the system in the same final state as doing it once. GET /users/123 is idempotent — asking ten times returns the same user and changes nothing. The danger case is a payment: without protection, clicking Pay twice charges the customer twice.

Analogy · recall

A light switch that's already ON. Flip it "on" again and again — the room's state never changes past the first flip. That's idempotent. A cash register that adds $100 every time you press the button is not.

The fix: an idempotency key#

The client generates a unique id — usually a UUID — for the operation and sends it in a header. The first time the server sees Idempotency-Key: abc-123, it processes the request and stores the result against that key. If the same key arrives again, the server recognises it and returns the stored response instead of processing a second time. The "memory" can live in a fast cache (Redis) or the database, depending on how durable it must be.

POST /payments — request headers + body
POST /payments HTTP/1.1
Content-Type: application/json
Idempotency-Key: abc-123

{ "amount": 500, "currency": "INR" }
1st call · key abc-123 Client Server: create store key 2nd call · same key Client key found → no new charge stored resp
fig 2.6 — same key, second time: server replays the stored response, never re-charges
⭐ TRICK · idempotency by verb

You don't memorise a table — you reason from the final state. GET, PUT, DELETE are naturally idempotent (reading, replacing to a fixed value, and deleting all land on the same end state however many times you repeat them). POST is not — each call creates a new resource. PATCH is the "it depends" one.

⚠ PITFALL · PATCH is conditional

PATCH /accounts/123 with { "name": "John" } is idempotent — the name is "John" no matter how often you send it. But { "increaseBalance": 100 } is not: each call adds 100, so the final state keeps changing (100 → 200 → 300). Same verb, opposite answer — the body decides.

⚠ PITFALL · store the key first

If the server processes the payment and only then tries to save the idempotency record, a failed write leaves no memory of the operation. The client retries with the same key, the server sees nothing stored, and charges again. Fix: write the idempotency key before processing, so the record survives even if the operation itself fails.

2.7Pagination — never ship the whole haystack#

Pagination returns a large result set in slices instead of one giant payload. The client asks for a window: GET /users?page=2&limit=20 means "the 20 records on page 2."

Think of an infinite feed — LinkedIn, Instagram. If you've been away for days there may be 500 new items waiting. Sending all 500 at once wastes bandwidth on content you'll never scroll to. Instead the server sends a page, and when you scroll to roughly 80% of it, it pulls the next page. You get a smooth feed; the system avoids paying to move data you'd never read.

Analogy · recall

A book delivered a chapter at a time. The publisher doesn't ship 500 pages if most readers stop at chapter two — they send the next chapter only once you're near the end of the current one.

2.8REST — the default style#

REST is a stateless, resource-oriented way for two applications to talk over HTTP. The interview one-liner: "REST is a stateless, resource-oriented API style built on top of HTTP." Data is usually exchanged as JSON.

It replaced SOAP, an older style that leaned on verbose XML — heavier on the wire and harder for a human to read. REST is lighter, simpler, and speaks the web's native vocabulary that every browser already understands.

💡 TIP · what "stateless" means

The server remembers nothing between requests. Each request is independent and must carry everything it needs — token, user id, and so on. That's what makes REST easy to scale horizontally: any server instance can handle any request, because none of them holds session state the others lack.

The two REST pains#

Because a REST endpoint's response shape is fixed by its contract, you hit two mirror-image problems.

Over-fetching#

  • You want one field (rank).
  • The endpoint returns 150 fields.
  • You pay for 149 you'll never use.

Under-fetching#

  • You want 4 fields spread across resources.
  • No single endpoint has all 4.
  • You make 3 calls and stitch them together.

A third, separate cost: JSON payloads are larger than a binary format. These three complaints are exactly what the next two styles were invented to fix — GraphQL for the fetch problems, gRPC for the payload size.

💡 TIP · when to reach for REST

Public APIs, web apps, mobile backends, straightforward CRUD, and anything integrating with a third party — roughly 85% of services still speak REST, so it's the safe common tongue. Avoid it for very high-frequency internal chatter, real-time streaming, or when the client needs highly customised data in one shot.

2.9gRPC — call a remote function like a local one#

gRPC lets one service call a method on another service as if it were a local function. It's a high-performance framework from Google that runs on HTTP/2 and serialises data with Protocol Buffers (a compact binary format) instead of JSON. The design goal: smaller payloads and lower latency for large microservice systems where REST-plus-JSON became the bottleneck.

Analogy · recall

Two colleagues who've agreed on a shorthand and share a phone line they never hang up. They don't spell everything out (binary, not prose), and they don't redial for every sentence (one persistent HTTP/2 connection). Faster than mailing postcards — but only because both learned the same shorthand.

How it works, in order#

  1. Define the service in a .proto file — this is the contract: service name, methods, request and response message shapes.
  2. A compiler generates client and server code automatically — the client stub, a server base class, and message classes. No hand-written networking.
  3. The client calls a method on the stub as if it were local (stub.add(req)).
  4. The request is serialised into compact binary.
  5. It travels over HTTP/2 — multiplexed (many requests share one connection), header-compressed, persistent.
  6. The server deserialises, runs the real logic, serialises the response, and sends it back.
  7. The client deserialises and uses the result.
calculator.proto — the gRPC contract
service Calculator {
  rpc Add (AddRequest) returns (AddResponse);
}

message AddRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message AddResponse {
  int32 result = 1;
}
client stub local call serialize → binary HTTP/2 deserialize → object run
fig 2.9 — the gRPC round trip; the response retraces the same path in reverse
⚠ PITFALL · gRPC is all-or-nothing

gRPC replaces the REST controller layer entirely — the API is defined in the .proto, and both client and server must speak gRPC. You can't have one service on gRPC talk to a REST-only third party. That's the main reason public and third-party integrations still use REST: it's the shared language 85% of the world already speaks.

💡 TIP · the serialization trade-off

Serialising and deserialising costs some CPU on both ends. You accept that overhead because the binary payload is far smaller than JSON, so it moves across the network faster and on less bandwidth — a net win on throughput and latency for chatty internal services (Uber, Netflix, Google-scale).

2.10GraphQL — the client orders exactly what it wants#

GraphQL is a query language for APIs where the client specifies exactly which fields it needs. Instead of many fixed endpoints, the application usually exposes one endpoint, and the query itself describes the desired shape. The interview one-liner: "GraphQL lets clients request only the fields they need, eliminating over- and under-fetching." It was created at Facebook to solve precisely the two REST pains.

Analogy · recall

À la carte ordering. REST is a fixed set menu — you take the whole plate whether or not you wanted the side. GraphQL lets you name each item: "just the name and email, nothing else," and that's exactly what arrives.

a GraphQL query — ask for two fields, get two fields
query {
  user(id: "123") {
    name
    email
  }
}

The server defines a schema (types and fields); the client sends a query; resolvers fetch just those fields from the right databases or services; the response contains only what was asked for — no over-fetch, and no second round trip to under-fetch.

⚠ PITFALL · the N+1 query problem

Fetch 100 users, then a separate query for each user's orders, and you've fired 101 queries instead of a sensible few. The fix is batching / a data loader that groups those child lookups. GraphQL moves flexibility to the client but pushes complexity — caching and authorization especially — onto the server.

2.11Choosing between the three#

There's no universally correct answer — the skill is justifying your pick out loud. State the advantages of what you chose; if you chose gRPC or GraphQL, first name the REST disadvantage it removes.

RESTgRPCGraphQL
Data formatJSONbinary (protobuf)JSON
TransportHTTPHTTP/2HTTP
Performancemediumvery highmedium
Browser-friendlyyeslimitedyes
Streamingweakstrongsubscriptions
Best usepublic API, CRUDinternal microservicesflexible client data
Origin—GoogleFacebook

Use REST for public APIs and normal CRUD. Use gRPC when internal services talk frequently and latency matters. Use GraphQL when a client needs flexible, customised data from multiple sources in a single request. And note the practical point from the session: services don't only talk via these styles — a message queue like Kafka handles asynchronous, decoupled communication, and truly independent services may not talk at all.

2.12Interview & practice Q&A#

Q1Why put an identifier in the path but a filter in the query string?

A path parameter identifies a specific resource — /users/123 is "the user whose id is 123." A query parameter modifies a collection by filtering, sorting, or paginating — /users?page=2&limit=20. Using the path for identity keeps URLs predictable and RESTful; stuffing an id into a query string (?id=123) muddies that distinction.

Q2A client double-clicks Pay. How do you stop a double charge?

Make the operation idempotent with a client-generated idempotency key (a UUID) sent in a header. The server stores the result keyed by that value; if the same key arrives again it returns the stored response rather than charging again. Store the key before processing so a mid-flight failure can't erase the record and let a retry slip through.

Q3Is PATCH idempotent?

It depends on the body. PATCH { "name": "John" } is idempotent — the end state is the same however many times you send it. PATCH { "increaseBalance": 100 } is not — each call adds 100, so the final state changes every time. Reason from the resulting state, not the verb.

Q4Why version an API instead of just editing v1?

Editing /v1 in place changes the contract under clients already using it, so their existing requests start failing. Publishing /v2 alongside /v1 lets new clients adopt the change and old clients migrate on their own timeline; you retire v1 only once nobody depends on it.

Q5What does "REST is stateless" buy you?

The server keeps no memory between requests, so every request carries all the context it needs (token, ids). Because no server instance holds session state the others lack, any instance can serve any request — which is what makes REST horizontally scalable behind a load balancer.

Q6Explain over-fetching and under-fetching, and which style fixes each.

Over-fetching: a fixed endpoint returns more fields than you need. Under-fetching: no single endpoint has everything, so you make several calls and combine them. GraphQL fixes both by letting the client request exactly the fields it wants in one query. (gRPC targets a different REST cost — large JSON payloads — by using a compact binary format.)

Q7Why is gRPC faster than REST?

Three reasons together: Protocol Buffers serialise data to compact binary (smaller than JSON), HTTP/2 multiplexes many requests over one persistent, header-compressed connection, and the connection stays open rather than being re-established per call. You pay some serialise/deserialise CPU, but win on payload size, bandwidth, and latency.

Q8Can a gRPC service integrate directly with a REST-only third party?

No. gRPC requires both ends to share the .proto contract and speak gRPC. If one side only offers REST, you either use REST for that boundary or put a translating layer in between. This is why third-party and public integrations default to REST — it's the shared language ~85% of services already speak.

Q9What's the N+1 problem in GraphQL, and how do you handle it?

Resolving a list of 100 users and then firing one query per user for their orders produces 101 queries. A data loader batches those child lookups into far fewer queries. It's the classic reminder that GraphQL's client flexibility shifts caching and query-efficiency burden onto the server.

2.13Key takeaways#

  • An API is a contract; design it around resources (nouns in the path, verbs as HTTP methods).
  • Status codes: first digit tells the story — 2xx you're fine, 4xx your fault, 5xx server's fault.
  • Idempotency = same request, same final state; reason from the state, use a key to make POST safe.
  • Store the idempotency key before processing, or a failed write lets a retry double-charge.
  • Version by publishing v2 beside v1; never rewrite a live contract in place.
  • Paginate large results; ship pages, not the whole set.
  • REST for public/CRUD, gRPC for fast internal microservices, GraphQL for client-chosen fields.
  • gRPC is all-or-nothing — both ends must speak it, which is why REST stays the common tongue.
contract noun URLs authN ≠ authZ 2/4/5xx idempotency key key-before-process v2 beside v1 stateless over/under-fetch protobuf + HTTP/2 N+1

3

Microexercises#

Reading proves you recognise the ideas. These prove you can use them. Attempt each before opening the walkthrough.

3.1Exercise 1 — design the endpoints (build-it)#

// your task

You're designing a payments service. Write the REST endpoint lines (method + path, plus any header) for the four operations below, following every design rule from §2.2 and §2.6.

  • Fetch a single payment by id.
  • List payments, 20 per page, page 3.
  • Create a payment safely against double-clicks.
  • Delete a payment by id.

Expected result: four lines using nouns in the path, correct verbs, the id in the path (not query), pagination in query params, and an idempotency-key header on the create.

Show the full walkthrough

Step 1 — nouns, not verbs. The resource is payments. Never write /getPayment or /createPayment; the HTTP verb already carries the action.

Step 2 — id in the path, filters in the query. A single payment is identified by its id, so it goes in the path: /payments/123. Paging is a filter on the collection, so it goes in the query string.

Step 3 — make create idempotent. POST is not idempotent by default, so a double-click could create two payments. Attach a client-generated Idempotency-Key in the header so a retry returns the stored result instead of charging twice.

the four endpoints
GET    /payments/123

GET    /payments?page=3&limit=20

POST   /payments
Idempotency-Key: 5f2c-uuid

DELETE /payments/123

Why: each line encodes a rule — resource nouns, the right verb, path-vs-query discipline, and the one header that turns an unsafe POST into a safe one.

3.2Exercise 2 — spot the idempotency bug (diagnose-it)#

// your task

A teammate added idempotency to the payment handler below. Under a network retry, customers are still occasionally charged twice. Predict what goes wrong, name the root cause, and fix the order of operations.

payment handler — pseudocode
def handle_payment(key, amount):
    result = charge_card(amount)        # 1. process first
    save_idempotency(key, result)       # 2. then remember the key
    return result

Expected result: identify that a failure between lines 1 and 2 causes a double charge on retry, then reorder so the key is recorded before the charge.

Show the full walkthrough

Root cause: the key is stored after the charge. If the process succeeds at line 1 but the save_idempotency write on line 2 fails (or the box crashes in between), there's no record that this key was ever handled.

Predicted failure: the client retries with the same key, the handler looks for the key, finds nothing stored, and calls charge_card a second time — the exact double charge idempotency was meant to prevent.

Step 1 — record intent first. Reserve the key before doing the work, so the memory survives a mid-flight failure.

Step 2 — check the key on entry. If it already exists, return the stored result and skip the charge entirely.

corrected handler
def handle_payment(key, amount):
    existing = lookup_idempotency(key)  # already seen?
    if existing:
        return existing.result          # replay, don't re-charge

    reserve_idempotency(key)            # 1. remember BEFORE processing
    result = charge_card(amount)        # 2. then process
    save_idempotency_result(key, result)
    return result

Why: idempotency only holds if the server's memory of the operation is durable across failures — storing the key before the side effect is what makes the retry safe.

nouns in path id → path, page → query key on POST reserve-then-charge check key on entry replay stored result