HTTP methods & status codes, done right
Published
Why this, first. Anyone can recite “GET reads, POST creates.” The senior bar is precise semantics: which methods are safe vs. idempotent, when PUT beats POST, and the exact status code for “well-formed but invalid.” These distinctions are the fastest discriminator an interviewer has between a mid and a senior answer — and they’re choices you defend in every design review afterward.
Three axes: safe · idempotent · cacheable
PUT vs POST = who owns the URI
Pick the exact status code
Never tunnel errors through 200
Three orthogonal properties (don’t conflate them)
Methods are described by three independent axes. 1 Mid-level answers blur them; seniors keep them separate. The containment to remember: safe ⊂ idempotent — every safe method is idempotent, but not the reverse.
GET may log or run analytics — it just isn't
intended to change resource state. Cacheability is independent of the other two axes.
PATCH is not guaranteed idempotent — JSON Merge Patch (RFC 7386) tends to
be; JSON Patch (RFC 6902) with ops like "add to array" is not. The senior move: make your PATCH
idempotent when you can, and know why it might not be.
PUT vs POST for creation
The classic probe. The deciding question is who owns the URI: 2
PUT replaces the whole resource;
PATCH applies a partial modification. A partial body with
PUT means "the omitted fields are now gone."
✓ Senior: lead with idempotency. POST isn’t idempotent, so a dropped-connection retry can double-create — that’s why creation uses PUT at a client-known URI, or POST plus an idempotency key.
Lead with idempotency, not the verb. That single sentence signals senior; the verb list alone signals junior.
Status codes that separate seniors
Pick the code that tells the client exactly what happened and what to do next. 3
200 with an error embedded in the body.
Don't tunnel errors through success. Location · 401 vs 403 is authn vs authz · 404 hides
existence on purpose · 307/308 preserve the method · PATCH idempotency depends on the patch ·
never tunnel errors through a 200.
201 vs 202 201 means done and created (with Location); 202 means
accepted for async processing — not done yet, hand back a status URL to poll.
304 is a feature Not Modified powers conditional GET / revalidation. It’s a cache win, not an error.
405 vs 409 vs 410 405 Method Not Allowed adds an Allow header; 409 for a
state conflict; 410 Gone for permanently removed.
429 + Retry-After Too Many Requests should tell the client when to come back — same courtesy as 503.
🎯 Memory rule: Three axes (safe ⊂ idempotent, cacheable apart); PUT vs POST = who owns the URI; pick the exact code — 422 not 400 for valid-but-invalid, 401 vs 403 for authn vs authz, 404 to hide existence — and never tunnel errors through a 200.
Retrieval practice
Close the lesson in your mind first, then answer from memory. Effortful recall is what converts this from “I read it” to “I own it.” Immediate feedback below.
Q1. Which property set is correct for the PUT method?
Q2. A request body is well-formed JSON but fails business validation. Return…
Q3. Why prefer 307 over 302 when redirecting a POST?
Q4. A successful 201 Created response MUST include which header?
Rehearse the answer out loud
Interviewer: "A client calls POST /payments and the connection drops before they get a response. Walk me through the method and status code design so a retry is safe." Type your answer from memory (no scrolling up), then reveal and compare — don't read the model first.
“POST is right here because the server assigns the payment id
— the client doesn’t know the URI ahead of time. If we process synchronously I return
201 Created with a Location header to the new payment; if it’s
async, 202 Accepted with a status/Location URL the client can poll.
The hard part is the dropped connection. POST isn’t idempotent, so a blind
retry risks a double-charge. The fix is a client-generated Idempotency-Key
header: the server records it, and a retry with the same key returns the original
result instead of charging again — that’s exactly the idempotency-in-practice pattern we
cover next. On a genuine state conflict I’d return 409; on a validation
failure 422. And I’d never return 200 with an error tucked in
the body.”
Why this scores: it names the non-idempotency problem explicitly and solves it with an idempotency key, picks the correct codes for sync vs async, and handles the failure branches (409/422) without tunneling errors through 200.
I’m your teacher — ask me anything. Want to drill the 401-vs-403 and 404-leakage trade-offs? Curious how JSON Patch breaks idempotency in practice? Want me to grade your rehearsal answer or fire harder follow-ups at you? Just say so in the chat.
Primary source (read this next)
📖
RFC 9110 — HTTP Semantics. The authoritative spec for method properties (safe/idempotent/cacheable) and status-code meaning. Keep the MDN methods and MDN status pages open as fast lookups.
Interview — pick your bar
Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).
Define a safe and an idempotent HTTP method.
Safe = read-only intent, no intended state change (GET, HEAD, OPTIONS). Idempotent = N identical requests have the same effect on server state as one (GET, HEAD, OPTIONS, PUT, DELETE). The key word is effect on state, not a byte-identical response.
Is POST idempotent? Why does it matter?
No. A retried POST can create a duplicate (a second charge or order). It matters because dropped connections and retries are normal in distributed systems — non-idempotency is exactly why POST creation needs an Idempotency-Key to be retry-safe.
200 vs 201 vs 204, and what must a 201 include?
200 OK: success with a body. 201 Created: a new resource was created and must carry a Location header to it. 204 No Content: success with no body (common for DELETE and for PUT/PATCH that don't echo the resource).
401 vs 403 — what's the difference?
401 Unauthorized = authentication failed ("we don't know who you are" — credentials missing/invalid/expired; re-authenticate). 403 Forbidden = authorization failed ("we know who you are, you're not allowed" — re-authenticating won't help).
Explain "safe ⊂ idempotent," and why "safe means secure" is false.
Every safe method is idempotent (reading changes nothing, so repeating it changes nothing); the reverse fails — PUT/DELETE are idempotent but not safe. "Safe" is about read-only intent, nothing about authn/authz/TLS: a GET can still expose sensitive data and still needs authorization and transport security. Keep safe / idempotent / cacheable as three separate axes.
Is PATCH idempotent, and how do JSON Merge Patch vs JSON Patch relate?
Not guaranteed — it depends on the patch document. A "set field to X" patch is idempotent; "increment by 1" / "add to array" is not. JSON Merge Patch (RFC 7386) sets values (null = remove), so it tends to be idempotent; JSON Patch (RFC 6902) is an op sequence where add isn't. Make PATCH idempotent when you can, and know why a given patch isn't.
Why prefer 307/308 over 302/301, and why is 304 a feature?
307/308 preserve the method and body; with 301/302 clients historically rewrote a POST into a GET, dropping the body. 304 Not Modified is the payoff of conditional GET: the client revalidates with If-None-Match/If-Modified-Since and, if nothing changed, gets 304 with no body and reuses its cache — bandwidth saved, not a failure.
When should a forbidden resource return 404 instead of 403?
When the very existence of the resource is sensitive. A 403 confirms "this exists, you just can't see it," which leaks information (resource enumeration — e.g. private repo names). 404 hides existence entirely. Trade-off: 404 is harder to debug for legitimate users, so reserve it for genuinely sensitive resources, not as a blanket policy.
A teammate puts "mark as read" behind GET /messages/42?read=true. What breaks?
It makes a state-changing operation safe-in-name only, and the ecosystem assumes GET is safe: prefetchers, crawlers, link-preview bots, and proxies fire it and silently mutate state, and caches may serve stale results. Intended state change belongs on a non-safe verb — POST, or PATCH/PUT for an idempotent update. The classic casualty was an admin panel where a crawler followed every "delete" link.
A downstream dependency is timing out under load. Walk the status codes and behavior you'd return.
If a circuit breaker is open / you're shedding load, return 503 + Retry-After so clients back off. If you actually called upstream and it timed out, 504 is honest; if it returned garbage, 502. Don't let upstream's timeout surface as a 500 for a valid client request, and never convert it into a 4xx — the client did nothing wrong. Combine with idempotency keys so the inevitable retries don't double-act.
Status code as the contract, or body error codes — how do you reconcile them?
Use both at the right layer. The HTTP status is the coarse, machine-actionable verdict that infrastructure (caches, LBs, retry logic, alerting) acts on — get it right first. The body carries a stable, fine-grained error code plus human/field detail for the application to branch on (e.g. "code": "card_declined" alongside a 402/422). Status for the network, body codes for the application — not one instead of the other.
- Model resources & URIs — nouns not verbs, collections vs items (
/orders,/orders/{id}), nesting only for true ownership. - Map methods & status codes — GET/POST/PUT/PATCH/DELETE to the right semantics; 2xx/4xx/5xx that mean what they say (201+
Location, 401 vs 403, 422 vs 400). - Idempotency & caching — safe/idempotent verbs, idempotency keys for POST,
ETag/If-Match/conditional requests. - Pagination, filtering & sorting — cursor vs offset, bounded page sizes, consistent query params.
- AuthN/AuthZ — bearer tokens on every request (stateless), scopes/roles, transport security, rate limits with
Retry-After(OWASP API Top 10). - Versioning & evolution — additive changes, deprecation policy, URL vs header versioning.
- Errors & contracts — status as the machine verdict plus a stable body error code (e.g. RFC 9457 problem+json), correlation IDs.
Design the request/response contract for POST /payments where the connection can drop mid-flight. Walk the methods and status codes.
A strong answer covers: POST is correct because the server assigns the payment id → sync path returns 201 Created + Location; async path returns 202 Accepted + a status URL to poll, never 200 for unfinished work → the dropped connection is the hard part: POST isn't idempotent, so a client-generated Idempotency-Key lets a retry replay the original result instead of double-charging → map the failure modes to honest codes: 422 for validation, 409 for a state conflict, 402/body error code for a declined card, 503+Retry-After when shedding load, 504 if upstream timed out → never tunnel an error through 200, never return 5xx for bad client input → name the contract split: status code for the network/infra, stable body error code for the application.
Design the update + concurrency story for a resource many clients edit at once (PUT vs PATCH, conditional requests).
A strong answer covers: choose PUT (full replace, idempotent by construction) vs PATCH (partial edit) by resource size and whether "set to null" must differ from "leave untouched" → for PATCH, pick the format deliberately — JSON Merge Patch (value-setting, tends to be idempotent) vs JSON Patch (op sequence, add isn't idempotent) → guard lost updates with optimistic concurrency: hand out an ETag on reads, require If-Match on writes, return 412 Precondition Failed on a stale version → distinguish 412 ("your version is stale") from 409 ("this action conflicts with current state") → make retries safe: idempotent verbs retry freely, and a turned-into-explicit conflict is retryable after a re-read → name the trade-off: PUT is simpler and retry-safe but bandwidth-heavy; PATCH saves bytes but adds format and idempotency nuance.
📄 Keep handy:
Constraints & method semantics reference
◂ Prev ·
Next ▸ Lesson 3These notes reflect my current understanding and are updated as I learn, build, and discover better explanations.