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.
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.
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 itCompare that with the shape people write when they think in function calls:
POST /createOrder
POST /getOrderById
POST /updateOrderStatus
POST /deleteOrderEverything 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/ordersis this customer's orders. Nest one level./customers/7/orders/42/lines/3/productis a URL nobody can construct from memory, and/orders/42identifies that order perfectly well on its own. - Actions that are not CRUD are still resources. "Cancel this order" is awkward as a
PATCH, andPOST /orders/42/cancellationreads 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:
| Method | Safe (changes nothing) | Idempotent (same result if repeated) |
|---|---|---|
GET, HEAD | yes | yes |
PUT, DELETE | no | yes |
POST | no | no |
PATCH | no | depends 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.
| Code | Means | The distinction that matters |
|---|---|---|
200 OK | here is the result | |
201 Created | made it, and Location says where | not 200 |
204 No Content | done, nothing to return | typical for DELETE and PUT |
400 Bad Request | I cannot parse or bind what you sent | malformed |
401 Unauthorized | I do not know who you are | sign in |
403 Forbidden | I know, and you may not | signing in again will not help |
404 Not Found | nothing at this path | |
409 Conflict | it clashes with the current state | duplicate, version conflict |
422 Unprocessable | parsed fine, the values are wrong | failed validation |
429 Too Many Requests | slow down; see Retry-After | |
500 | my fault | |
503 Service Unavailable | my fault, and temporary | a retry may work |
A POST that creates something should say so, and say where it went:
$ 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:
$ curl -i -X DELETE /customers/1
HTTP/1.1 204No 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.ETagandIf-None-Match— the caching lesson's subject; a client can ask "has this changed?" and get304with no body.Retry-After— on429and503, how long to wait. Without it a client guesses, and the guess is usually "immediately".Content-Type— what you are sending.application/problem+jsonfor errors is a real convention and the error lesson uses it.