Skip to content
QSWEQB
mediumConceptEntryMidSenior

Which status codes and method semantics do you insist on in a code review, how do the caching headers fit together, and is the API you just described REST?

Get the method's safety, idempotency and cacheability right, use the status code intermediaries and clients act on rather than a generic 200 with an error body, pair Cache-Control freshness with ETag or Last-Modified validators, and be honest that most APIs called REST are resource-shaped HTTP without hypermedia.

5 min readUpdated 2026-07-26

What the interviewer is scoring

  • Whether safety, idempotency and cacheability are treated as three separate properties of a method
  • Does the candidate distinguish 401 from 403 and 400 from 422 on the basis of what the client should do next
  • That no-cache and no-store are not confused, since one permits storage and the other forbids it
  • Can they use a validator for optimistic concurrency, not only for bandwidth saving
  • Whether they can describe the REST constraints and then say plainly which ones their API does not satisfy

Answer

Three properties, not one

Method semantics are graded on whether you keep three independent properties apart, because conflating them is the commonest error in API review.

A method is safe if the client is not requesting a state change — GET and HEAD, which is why a crawler or a browser prefetcher will call them without asking. A method is idempotent if sending it repeatedly has the same intended effect as sending it once, which covers the safe methods plus PUT and DELETE. A method is cacheable if a response to it may be stored and reused. GET is all three. PUT and DELETE are idempotent but neither safe nor usefully cacheable. POST and PATCH are none of them by declaration, though a POST handler can be made idempotent with a client-supplied key and that is a legitimate design rather than a violation.

The declaration is about the method, not about your handler. Nothing stops you writing a PUT that increments a balance, and no intermediary will save you when it retries. What the declaration means in practice is that clients, proxies and libraries take a licence to retry idempotent methods automatically, so honouring the semantics is your obligation. The second-order point worth making is that idempotency constrains state, not responses: a repeat DELETE answering 404 is still idempotent, because the resource is absent either way.

The status codes that change behaviour

Most of the code list is trivia. A handful are worth being strict about because something other than a human reads them.

On success, 201 with a Location header for a create tells the client where the thing went; 202 says you have accepted work you have not finished, and it obliges you to give the client somewhere to look; 204 says success with deliberately no body, and returning it with a body is simply malformed.

On client error, the distinction that matters is what the client should do differently. 400 means the request is malformed and cannot be parsed or is structurally invalid. 422 Unprocessable Content, registered in RFC 9110, means the syntax was fine but the content failed your domain rules — a well-formed JSON body with a start date after its end date. 401 means unauthenticated and is required to carry a WWW-Authenticate header saying how to authenticate; 403 means authenticated and not permitted, so retrying with the same credentials is pointless. 409 signals a conflict with current state that the client could resolve and retry, 412 signals a precondition you were given did not hold, and 429 signals rate limiting and should carry Retry-After.

On server error, 500 means an unexpected failure and 503 means you are unavailable but expect to return, which with Retry-After is the difference between a client backing off sensibly and a client hammering you.

The anti-pattern to name is returning 200 with {"error": "not found"}. It breaks every intermediary that reasons about status, breaks generic client retry logic, makes error rates invisible to monitoring that counts 5xx, and produces the situation where a cache happily stores your error for an hour. Use the status line for the class of outcome and a structured body for the detail — RFC 9457's problem-details format is a reasonable off-the-shelf shape rather than inventing your own error envelope.

Freshness and validation are two mechanisms

Caching has two halves and they answer different questions. Cache-Control sets freshness: how long a response may be reused without asking. Validators enable revalidation: how a cache asks whether a stale copy is still good, cheaply.

For freshness, max-age applies to any cache and s-maxage overrides it for shared ones, which is how you keep something for an hour in your CDN and ten seconds in the browser. private marks a response as belonging to one user and forbids a shared cache storing it. no-cache is the directive people get wrong: it permits storage and requires revalidation before reuse. no-store is the one that means what people think no-cache means — do not write this anywhere. And when you send no freshness information at all, a cache is permitted to guess a lifetime heuristically, so silence is not the same as opting out.

For validation, an ETag is an opaque token for a representation. The client returns it in If-None-Match; if it still matches you answer 304 with no body and the client reuses what it has. Last-Modified and If-Modified-Since do the same job with one-second granularity, which is why an ETag is preferable for anything that changes faster than that. A weak validator, prefixed W/, asserts semantic equivalence rather than byte equality, so it can be used for revalidation but not for byte-range requests.

Vary is the header that ruins days when omitted. If a response differs by Accept-Language or Authorization and you do not declare it, a shared cache is entitled to serve one user's response to another. Varying on Authorization is usually the wrong fix, because the right fix is private.

Validators are your concurrency control

The half of this that most candidates never mention is that the same validator solves the lost-update problem, and it is the detail that reads as experience rather than revision.

PUT /invoices/4412 HTTP/1.1
If-Match: "a3f1c9"
Content-Type: application/json

{"status": "paid"}

The client is saying: apply this only if the representation is still the one I read. If someone else has written since, the tag no longer matches and you return 412 Precondition Failed, and the client reloads and reapplies rather than silently overwriting a change it never saw. This is optimistic concurrency expressed in HTTP rather than bolted on as a version field in the body, and for a resource where concurrent edits are possible you can go further and reject an unconditional PUT with 428 Precondition Required.

What REST requires, and what people call REST

Fielding's constraints are client–server separation, statelessness, cacheability, a uniform interface, a layered system, and optionally code-on-demand. Two of those are where real APIs part company with the label.

Statelessness means the request carries everything needed to service it, so no server-side session affinity — a bearer token satisfies this, a sticky in-memory session does not. The uniform interface is the demanding one: resources identified by URIs, manipulated through representations, self-descriptive messages, and hypermedia as the engine of application state. That last part means the client discovers what it can do next from links in the response rather than from documentation and hardcoded URL templates.

Almost nothing described as REST does this. On the Richardson maturity model most production APIs sit at level two: resources with sensible URIs and correct method use, no hypermedia. That is a perfectly reasonable engineering choice — hypermedia's benefit is decoupling clients from URL structure, which pays off when you do not control the clients and rarely otherwise. The graded behaviour is not conforming, it is knowing. Say "this is resource-oriented HTTP at level two, and we version the path because clients hardcode URLs" and you have demonstrated you understand the constraint you chose to skip. Claiming full REST for an API with a /getUserById endpoint and a server-side session is how you lose the point.

Likely follow-ups

  • A client sends If-Match with a stale ETag on a PUT. What do you return, and what should the client do with it?
  • Why is 404 sometimes the right answer for a resource that exists?
  • Your API sits behind a CDN and responses differ per authenticated user. What must you set, and what happens if you forget?
  • When is POST the correct method for something that reads data?

Related questions

Further reading

httprestcachingetagstatus-codes