HTTP and API Styles: Caching, REST, gRPC and GraphQL
What HTTP methods, status codes and caching headers promise, and when to pick REST, gRPC or GraphQL.
- 304Unchanged: saves the body, not the round trip
- N+1 → 1Queries after batching GraphQL lookups
- 429 + Retry-AfterHow to tell clients to slow down
Show what to say
“GET, PUT and DELETE are idempotent so they are safe to retry; POST needs an idempotency key. Caching works through max-age and ETag revalidation, where a 304 saves the bytes. REST is the default for public APIs, gRPC suits internal low-latency or streaming calls, and GraphQL suits many clients needing different fields, if I handle N+1 and cost limits.”
Trade-offs
- REST vs gRPC: REST is universal and cacheable by URL; gRPC is smaller, typed and streams but needs a proxy for browsers.
- REST vs GraphQL: fixed endpoints are simple to cache and secure; GraphQL fits flexible clients but adds N+1, cost limits and weak HTTP caching.
- Offset vs cursor pagination: offset is simple but skips and repeats rows as data changes; a cursor is stable and fast.
Bring it up before they ask
Retrying a timed-out POST can charge twice, so mention the idempotency key before the interviewer asks about failure.
HTTP gives you methods with promises (safe, ), status codes, and through Cache-Control and ETags. REST is resources over HTTP, is typed binary calls over , and lets the client choose the fields; each fixes one problem and brings its own.
HTTP semantics that interviews probe
Methods carry promises that clients, and retries rely on.
Read more · 170 words
Safe means it does not change anything; means doing it twice leaves the same state as doing it once.
| Method | Safe | Idempotent |
|---|---|---|
| GET | yes | yes |
| PUT, DELETE | no | yes |
| POST | no | no |
PATCH is not guaranteed idempotent. Status classes: 2xx worked, 3xx look elsewhere, 4xx the client erred (401 not logged in, 403 not allowed, 404 missing, 409 conflict, 429 slow down), 5xx the server erred (502 and 504: a gateway got a bad or no answer upstream).
Caching and conditional requests
Cache-Control: max-age=60 lets the browser reuse a response for 60 seconds with no request.
Read more · 42 words
After that it revalidates: it sends the ETag it saved as If-None-Match, and an unchanged resource answers 304 with no body.
A 304 saves the bytes, not the round trip. Use max-age for things that change rarely, and no-store for private data.
- Client
- Service
Text description
import { createHash } from 'node:crypto';
// An ETag is a fingerprint of the response body. The browser sends it back in If-None-Match,
// and if nothing changed the server answers 304 with no body: the bytes are saved, not the round trip.
export const etagOf = (body) => `"${createHash('sha1').update(body).digest('hex').slice(0, 16)}"`;
export function respond(body, requestHeaders = {}) {
const etag = etagOf(body);
const headers = { etag, 'cache-control': 'max-age=60' }; // fresh for 60 s: no request at all
if (requestHeaders['if-none-match'] === etag) return { status: 304, headers };
return { status: 200, headers, body };
}REST: resources and verbs
REST models the API as resources (nouns with URLs) acted on by HTTP verbs: GET /orders/42, POST /orders.
Read more · 87 words
Requests are : each carries everything the server needs, so any instance can answer. That is what makes REST easy to , load balance and debug.
Its weaknesses are chattiness (several round trips for one screen) and fixed response shapes. More in the HLD path.
gRPC: typed contracts over HTTP/2
defines services in a .proto file, generates client and server code, and sends compact binary protobuf over .
Read more · 67 words
It supports unary calls and server, client and bidirectional streaming, and every call carries a deadline that propagates through the chain of services.
It shines between internal services. The costs: browsers need a proxy (gRPC-Web), payloads are not human-readable, and URL-based is lost.
GraphQL: ask for exactly what you need
exposes one endpoint and a typed .
Read more · 112 words
The client sends a query naming the fields it wants, so a phone screen and a dashboard each get their own shape from one API, with one round trip.
The classic trap is N+1: a list of 100 posts, and each post's author field runs its own query. A loader batches every lookup in the same tick into one call.
Also plan for query cost and depth limits, per-field authorization, and (everything is a POST to one URL, so cache by object ID in the client instead).
// The N+1 problem: a list of 100 posts, and each post's author is fetched with its own query.
// A loader collects every load() made in the same tick and sends ONE batched call instead.
// Results are also cached per request, so the same key is fetched only once.
export function makeLoader(batchFn) {
let queue = [];
const cache = new Map();
return function load(key) {
if (cache.has(key)) return cache.get(key);
const promise = new Promise((resolve, reject) => {
queue.push({ key, resolve, reject });
if (queue.length === 1) {
queueMicrotask(async () => {
const batch = queue; // everything asked for in this tick
queue = [];
try {
const values = await batchFn(batch.map((b) => b.key));
batch.forEach((b, i) => b.resolve(values[i]));
} catch (err) {
batch.forEach((b) => b.reject(err));
}
});
}
});
cache.set(key, promise);
return promise;
};
}Choosing between them
Is it a public API, or called from browsers and third parties?
Idempotency and retries
Networks fail after the server has acted but before the client hears.
Read more · 143 words
The client cannot tell, so it retries. For GET, PUT and DELETE that is safe. For POST /payments it can charge twice.
The fix is an : the client sends a unique Idempotency-Key header, and the server stores the result under that key and returns it for any repeat. Pair retries with and , and only retry on errors that are plausibly transient (timeouts, 502, 503, 504, 429 with Retry-After).
Errors, versioning and pagination
Read more · 164 words
- Errors. Return a consistent body (a code, a message, a request ID) with the right status. Never return 200 with an error inside.
- Versioning. Prefer additive changes. When you must break, version in the URL (
/v2/orders) or a header, and keep the old one alive until clients move. - Pagination. Offset pagination skips and duplicates rows when data changes and gets slow on deep pages. Cursor pagination ("after this id") is stable and fast.
- . Answer 429 with
Retry-Afterso well-behaved clients back off.
Follow-up questions
Which HTTP methods are idempotent, and why does it matter?
GET, PUT, DELETE (and HEAD, OPTIONS) are . It matters because clients and proxies may retry them after a timeout. POST is not, so it needs an .
How does HTTP caching work?
Cache-Control says how long a response is fresh. When stale, the client revalidates with If-None-Match and the server returns 304 if the ETag still matches.
REST or gRPC for internal services?
for latency-sensitive, high-volume or streaming calls where both ends are your code. REST where humans, browsers or partners call it, or where HTTP matters.
What is the N+1 problem in GraphQL and how do you fix it?
A list resolver triggers one query per item for a nested field. Batch the lookups with a loader so the whole list costs one query.
Check yourself
Question 1 of 7Score 2 out of 3 or better to mark this page complete.