REST API Networking

intermediate rest api idempotency caching etag http

REST is more a style than a protocol — but the way it uses HTTP creates real network-level behaviors we should know: statelessness, idempotency, and caching.

In simple language: each REST request stands alone (no server-side session needed), some methods are safe to retry, and clever headers let us skip downloading data we already have.

Statelessness

Every REST request must contain everything the server needs to handle it. No “remember what I asked last time” — the server has no per-client memory.

Why we care:

  • Any server can handle any request → trivial horizontal scaling.
  • Load balancers don’t need sticky sessions.
  • Easier to cache, easier to retry.

If we need state (logged-in user), we send it on each request — usually as a token in the Authorization header or a cookie.

Idempotency

A method is idempotent if calling it once or 100 times has the same effect on the server. Crucial for retries on flaky networks.

MethodSafe?Idempotent?Notes
GETyesyesRead-only
HEADyesyesSame as GET, no body
OPTIONSyesyesUsed for CORS preflight
PUTnoyesReplaces resource — same input → same end state
DELETEnoyesAlready gone after the first call
POSTnonoCreating a new resource each time
PATCHnonot alwaysDepends on the patch ({x: 5} yes, {x: x+1} no)

Why this matters: if our HTTP client retries on timeout, it should only retry idempotent methods automatically. Retrying a POST might create two orders.

To make POST safer, we use an Idempotency-Key header (Stripe-style):

POST /payments HTTP/1.1
Idempotency-Key: 7c8a4b9e-1234-...
Content-Type: application/json

{"amount": 1000}

Server caches the result keyed by that ID and returns the same response if we retry.

Caching Headers

HTTP caching can save us from re-fetching unchanged data.

Cache-Control

Sets the cacheability rules.

Cache-Control: public, max-age=3600     # cache for 1 hour, anyone can cache
Cache-Control: private, max-age=60      # only the browser, not CDNs
Cache-Control: no-store                 # don't cache at all (sensitive data)
Cache-Control: no-cache                 # cache, but always revalidate

ETag and If-None-Match

A short fingerprint of the response. The browser sends it back to ask “still the same?”

Last-Modified and If-Modified-Since

Same idea but using a timestamp.

The 304 Not Modified Flow

1st request:
  GET /users/42
  → 200 OK
    ETag: "abc123"
    Cache-Control: max-age=60
    {body...}

2nd request (after max-age expires):
  GET /users/42
  If-None-Match: "abc123"
  → 304 Not Modified
    (no body — browser reuses cached copy)

The 304 response is tiny — just headers, no body. Saves bandwidth and time when data hasn’t changed.

// Express example serving an ETag
app.get("/users/:id", (req, res) => {
  const user = getUser(req.params.id);
  const etag = hash(user); // short fingerprint
  res.set("ETag", etag);

  // If client already has this version, send 304
  if (req.headers["if-none-match"] === etag) {
    return res.status(304).end();
  }
  res.json(user);
});

REST vs RPC On the Wire

Quick contrast:

  • REST — resource-centric URLs (/users/42), HTTP verbs as actions, leverages HTTP caching and status codes natively.
  • RPC (gRPC, JSON-RPC) — function-centric (/UserService/GetUser), usually a single verb (POST), bypasses HTTP semantics.

REST plays nicer with browsers, CDNs, and tooling because it speaks HTTP fluently. RPC tends to be more efficient and strongly typed but gives up cache-friendliness.

Interview Tip

When asked “is POST idempotent,” the right answer is “no by default, but we make it idempotent at the application level using an idempotency key.” Understanding that nuance separates juniors from mids.