• Lesson 2 of 11
  • 5 min read
  • Lesson

HTTP Methods & Status Codes Explained

HTTP methods and status codes explained: GET, POST, PUT, PATCH, DELETE semantics, safe vs idempotent methods, and choosing the right 2xx/4xx/5xx code for REST APIs.

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 · cacheablePUT vs POST = who owns the URIPick the exact status codeNever 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.

SAFE ⊂ IDEMPOTENT · CACHEABLE IS A SEPARATE AXISIDEMPOTENT — repeat = same stateSAFE — read-only intentGET · HEADOPTIONScacheable by default →PUTDELETEchange state, but convergeNOT IDEMPOTENTPOSTPATCH**depends on the patch doc
"Safe" ≠ "secure": a GET may log or run analytics — it just isn'tintended to change resource state. Cacheability is independent of the other two axes.
METHOD SEMANTICS MATRIX · RFC 9110Safe?Idempotent?Has body?GET✓✓✗POST✗✗✓PUT✗✓✓PATCH✗maybe✓DELETE✗✓often
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

WHO OWNS THE TARGET URI?Does the client know the URI?YESPUT /users/42replace-or-create at this addressidempotent — two calls → one user 42NOPOST /usersserver assigns the URIreturns 201 + Location header
The other axis: PUT replaces the whole resource;PATCH applies a partial modification. A partial body withPUT means "the omitted fields are now gone."
✗ Myth: “lead with the verb list — GET reads, POST creates, PUT updates.”

✓ 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

STATUS-CODE FAMILIES AT A GLANCE2xx · Success200 OK — success with a body201 Created — MUST add Location header202 Accepted — async, not done yet204 No Content — success, empty body3xx · Redirection301/308 permanent · 302/307 temporary307/308 preserve method + body301/302 may switch POST → GET304 Not Modified — a feature, not error4xx · Client error400 malformed · 422 well-formed, invalid401 who are you? · 403 not allowed404 hidden/absent · 410 gone · 409 conflict405 +Allow · 429 +Retry-After5xx · Server error500 generic · 502 bad gateway503 unavailable (+Retry-After)504 gateway timeoutNever 5xx for a client's bad input
The cardinal anti-pattern: returning 200 with an error embedded in the body.Don't tunnel errors through success.
PICK THE RIGHT CODE — FOUR FORKSCreated something new?→ 201 + Location (new resource)→ 200 if returning a body, no new URI→ 204 success, nothing to send backtypical 204: DELETE / PUT with no bodyBad request — how bad?→ 400 syntactically broken / unparseable→ 422 parses fine, fails validation"well-formed JSON, business rule fails"= 422 Unprocessable ContentAuth problem — which?→ 401 "we don't know who you are"authentication — credentials missing/bad→ 403 "we know you, you can't"authorization — identity is fine, no accessNot here — for now or forever?→ 404 absent, or hiding existence404 over 403 avoids leaking it exists→ 410 Gone — permanently removedtells clients to stop asking
The gotcha checklist: 201 needs 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?
  • it is both safe and also fully cacheable
  • it is safe but it is never idempotent
  • it is idempotent but it is not safe
  • it is neither idempotent nor ever cacheable
Q2. A request body is well-formed JSON but fails business validation. Return…
  • a 500 because the request could not complete
  • a 403 because the data was clearly forbidden
  • a 400 because the request looked plainly broken
  • a 422 because the request was semantically invalid
Q3. Why prefer 307 over 302 when redirecting a POST?
  • because 307 permanently caches the redirect target longer
  • because 307 preserves the original method and body
  • because 307 tells the client to switch into GET
  • because 307 signals the resource has been removed
Q4. A successful 201 Created response MUST include which header?
  • a Location header pointing at the new resource
  • a Retry-After header telling clients when to retry
  • an Allow header listing every permitted request method
  • a Cache-Control header marking the response cacheable

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.

Reveal model answer

“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.

REST design-round framework — drive any "design a REST API" prompt through these, out loud:
  1. Model resources & URIs — nouns not verbs, collections vs items (/orders, /orders/{id}), nesting only for true ownership.
  2. 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).
  3. Idempotency & caching — safe/idempotent verbs, idempotency keys for POST, ETag/If-Match/conditional requests.
  4. Pagination, filtering & sorting — cursor vs offset, bounded page sizes, consistent query params.
  5. AuthN/AuthZ — bearer tokens on every request (stateless), scopes/roles, transport security, rate limits with Retry-After (OWASP API Top 10).
  6. Versioning & evolution — additive changes, deprecation policy, URL vs header versioning.
  7. 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.