Skip to main content

HTTP Verbs, Status Codes, and Idempotency

What This Concept Is

HTTP defines verbs (methods) with standardized semantics. Your API inherits those semantics whether you want to or not - caches, proxies, libraries, and consumers will assume them. The big five plus PATCH:

VerbSafe?Idempotent?Typical use
GETyesyesread a resource or list
HEADyesyeslike GET but headers only
POSTnonocreate in a collection; non-idempotent ops
PUTnoyesreplace a resource at a known URL
PATCHnono (RFC)partial update; can be made idempotent
DELETEnoyesremove a resource

Safe means no server-side side effect the caller is responsible for (GET reading is fine; incrementing a view counter is not a user-visible side effect and is allowed).

Idempotent means calling the same request twice has the same server state as calling it once. Idempotency is what lets clients retry on network failure without double-charging anyone.

Status codes are grouped:

  • 2xx success - 200 OK, 201 Created, 202 Accepted, 204 No Content
  • 3xx redirect - 301, 304 Not Modified
  • 4xx client error - 400, 401, 403, 404, 409 Conflict, 410 Gone, 422, 429 Too Many Requests
  • 5xx server error - 500, 502, 503, 504

Do not invent status codes outside the standard. Do not reuse 500 for validation errors.

Why It Matters Here

Verbs and status codes are the only part of your API that HTTP infrastructure (CDNs, proxies, load balancers, client retry libraries) actually understands. If you use them correctly, you get caching, retries, and error classification for free. If you misuse them, every layer misbehaves.

Idempotency in particular is not a nice-to-have. Any distributed system has retries; any API without idempotency semantics invites duplicate side effects.

Concrete Example

A correct CRUD pattern for an Order resource:

POST   /orders                     -> 201 Created,  body=Order, Location: /orders/ord_1
GET /orders/ord_1 -> 200 OK, body=Order
GET /orders -> 200 OK, body={ items: [...], next_page_token }
PUT /orders/ord_1 -> 200 OK, body=Order (full replace)
PATCH /orders/ord_1 -> 200 OK, body=Order (partial update)
DELETE /orders/ord_1 -> 204 No Content

Error cases and their status codes:

POST /orders   malformed JSON          -> 400 Bad Request
POST /orders missing required field -> 400 Bad Request (or 422 Unprocessable)
GET /orders/xyz not found -> 404 Not Found
PUT /orders/ord_1 stale If-Match -> 412 Precondition Failed
POST /orders duplicate idempotency key but different body -> 409 Conflict
POST /orders rate-limited -> 429 Too Many Requests, Retry-After: 30
GET /orders/ord_1 deleted -> 410 Gone (not 404 if we want to signal it existed)

Making POST idempotent via a header (standard pattern):

POST /orders
Idempotency-Key: 4b2e2c01-7c2f-4a2a-8f4f-92bfb0d0e1b1
Content-Type: application/json

{ "customer_id": "c_9", "items": [ ... ] }

Server behavior:

  • first call: creates the order, stores (key, request_hash, response) for 24 hours, returns 201
  • retry with same key and same body: returns the cached 201 with the same Order - no duplicate
  • retry with same key but different body: returns 409 Conflict
  • after TTL: key is reusable

For updates, prefer PUT (naturally idempotent: send the full state). For partial updates, PATCH can be made idempotent by including preconditions such as If-Match: "etag-value":

PATCH /orders/ord_1
If-Match: "W/\"v12\""
Content-Type: application/json-patch+json

[{ "op": "replace", "path": "/status", "value": "confirmed" }]

Retries with the same If-Match either succeed once or fail with 412; retries never double-apply.

Common Confusion / Misconception

"POST is for create." That is the common case, not the rule. POST is "non-idempotent operation on a collection or non-resource endpoint." Creating is one example; running a search with a large filter (POST /orders:search) is another.

"PUT and PATCH do the same thing." PUT replaces the entire resource; omitting a field means "set it to default / null." PATCH updates a subset; omitted fields are unchanged. Use PUT when the client has the full state; PATCH when it has only the diff.

"DELETE is idempotent, so I can return 204 always." You should still distinguish: first DELETE returns 204 (deleted); second returns 204 or 404 (already gone). Either is valid per spec; pick one and document it.

"200 is the safe choice for everything." 200 with an error body is a common anti-pattern. HTTP-aware infrastructure treats 2xx as success; your client's retry logic will not retry a 200 { ok: false }. Return the correct status.

"GET with a request body is fine for search." Per HTTP spec, servers MAY ignore GET bodies, and many proxies and CDNs strip them. Use query parameters or a POST /things:search custom method (Cluster 3).

How To Use It

For each endpoint:

  1. Pick the verb by answering: does it read or write? Is it idempotent? Does it target a collection or an item?
  2. Pick the success status by answering: is there a body (200)? Was something created (201)? Was it accepted for async processing (202)? No body (204)?
  3. Map every failure to a specific 4xx or 5xx - never 200 { ok: false }.
  4. Decide idempotency for every write. Document the mechanism (Idempotency-Key, If-Match, or "naturally idempotent because PUT").

Keep a one-page verb-and-status table per API. Review it in contract review.

Check Yourself

  1. Why is POST for creation not idempotent by default, and what are the two common ways to make it idempotent?
  2. A consumer sees 504 Gateway Timeout on POST /payments. What should they do? What would your answer be if it were POST /search?
  3. Is returning 200 OK with an error body in the response wrong, or just unconventional? Under what conditions does it actively break things?

Mini Drill or Application

For a banking API with accounts, transfers, statements:

  1. Produce a table with verb, URL, success status, idempotency mechanism for each endpoint.
  2. Produce a second table with one expected 4xx and one expected 5xx per endpoint, naming the code and meaning.
  3. Write a paragraph defending the idempotency choice for POST /transfers.

Read This Only If Stuck