Resources, methods and status codes

Nouns, not verbs; PUT versus PATCH; 200, 201, 204, 400, 404, 409, 422 — and the difference a correct code makes to a client's retry logic.

6 min read🔗 REST API Engineering

HTTP already has opinions about what a request means. A REST API that agrees with them gets retries, caching and error handling from clients it has never met; one that does not gets a client library full of special cases with your service's name on it.

This lesson is the part of the contract you cannot change later without breaking somebody.

Resources are nouns

A resource is a thing, and the URL names it. The method says what you are doing to it.

plaintext
GET    /orders           list them
POST   /orders           create one
GET    /orders/42        read one
PUT    /orders/42        replace it
PATCH  /orders/42        change part of it
DELETE /orders/42        remove it

Compare that with the shape people write when they think in function calls:

plaintext
POST /createOrder
POST /getOrderById
POST /updateOrderStatus
POST /deleteOrder

Everything is a POST, so nothing can be cached, nothing can be safely retried, and every client has to read your documentation to learn what any of it does. The verb is already in the request line; putting it in the path as well throws away everything HTTP knows.

Two shapes worth having ready:

  • Nesting expresses ownership: /customers/7/orders is this customer's orders. Nest one level. /customers/7/orders/42/lines/3/product is a URL nobody can construct from memory, and /orders/42 identifies that order perfectly well on its own.
  • Actions that are not CRUD are still resources. "Cancel this order" is awkward as a PATCH, and POST /orders/42/cancellation reads honestly: you are creating a cancellation. The alternative — POST /orders/42/cancel — is a verb, and it is the pragmatic choice most teams make. Pick one and be consistent; the cost of mixing is higher than the cost of either.

Safe, idempotent, neither

Two properties decide what a client, a proxy or a retry may do without asking you:

MethodSafe (changes nothing)Idempotent (same result if repeated)
GET, HEADyesyes
PUT, DELETEnoyes
POSTnono
PATCHnodepends on what you wrote

Idempotent does not mean "returns the same response". DELETE /orders/42 twice returns 204 then 404, and that is fine — the state of the server is identical either way. The order is gone.

POST is the one that is not idempotent, and that is the whole reason the idempotency lesson later in this course exists: a client that times out and retries a POST has no way to know whether the first one arrived. Two orders. Two charges.

PATCH is idempotent only if you wrote it that way. {"status": "SHIPPED"} is idempotent. {"op": "increment", "field": "attempts"} is not.

Status codes a client can act on

The first digit is the category and 4xx versus 5xx says whose fault it is — which is what decides whether a client retries and whether your alerting fires.

CodeMeansThe distinction that matters
200 OKhere is the result
201 Createdmade it, and Location says wherenot 200
204 No Contentdone, nothing to returntypical for DELETE and PUT
400 Bad RequestI cannot parse or bind what you sentmalformed
401 UnauthorizedI do not know who you aresign in
403 ForbiddenI know, and you may notsigning in again will not help
404 Not Foundnothing at this path
409 Conflictit clashes with the current stateduplicate, version conflict
422 Unprocessableparsed fine, the values are wrongfailed validation
429 Too Many Requestsslow down; see Retry-After
500my fault
503 Service Unavailablemy fault, and temporarya retry may work

A POST that creates something should say so, and say where it went:

plaintext
$ curl -i -X POST /customers -d '{"name":"Bo","email":"bo@example.com","age":30}'
HTTP/1.1 201
Location: /customers/2
Content-Type: application/json
 
{"id":2,"name":"Bo","email":"bo@example.com"}

201 plus Location means a client can follow the link without parsing your body to find the id. And a delete that returns nothing should return nothing:

plaintext
$ curl -i -X DELETE /customers/1
HTTP/1.1 204

No body. Not {}, not {"success": true} — 204 already said it.

The three pairs people get wrong

401 versus 403. 401 is about identity, 403 about permission. Returning 403 to an unauthenticated user sends them to a "contact your administrator" screen when they simply needed to sign in.

400 versus 422. 400 means I could not read this — malformed JSON, a string where a number belongs. 422 means I read it perfectly and the values are not acceptable — an email that is not an email, an age of 12. The distinction matters because they lead to different client behaviour: 400 is a bug in the client, 422 is something to show the user. Decide once, write it down, and be consistent; the next lesson on validation makes this concrete.

404 versus 403 for something you may not see. If /orders/42 exists but belongs to somebody else, 403 confirms it exists. For anything sensitive, 404 is the safer answer — and that is a deliberate choice worth recording, not an accident.

Headers carry the parts that are not the body

  • Location — where the thing you just created lives.
  • ETag and If-None-Match — the caching lesson's subject; a client can ask "has this changed?" and get 304 with no body.
  • Retry-After — on 429 and 503, how long to wait. Without it a client guesses, and the guess is usually "immediately".
  • Content-Type — what you are sending. application/problem+json for errors is a real convention and the error lesson uses it.
Progress is saved on this device and to your account when signed in.