REST gets a bad reputation because most arguments about it are about theology, not mechanics. Strip away the dogma and what remains is a set of concrete engineering decisions: how you represent state, how you address it, and how you signal the outcome of a request. All three are load-bearing — clients are built on top of them. This article walks the mechanics of resource modeling: when to model a noun vs. an action, how far nesting should go, and what your status codes actually promise to the client.
Resources vs actions: the shape of the state machine
A resource is a named state that you can read and manipulate over HTTP. The discipline of modeling is deciding which pieces of state deserve to be resources and which are better expressed as operations.
The test is simple: does this thing have an identity, a lifecycle, and multiple clients that refer to it? An order has all three. "Send the order to the warehouse" does not — it is a transition in the order's lifecycle, and modeling it as a sub-resource or a state transition is more honest than a noun.
POST /orders/48211/cancelPOST /orders/48211/cancellationsThe first is an action; the second is a resource that happens to represent a cancellation event. Both are legitimate — the deciding factor is whether other endpoints will ever need to refer to the cancellation. If you need "list all cancellations for this account," the resource version wins. If it's a terminal, irreversible operation, the action version is fine and often clearer.
Nested resources: depth has a cost
GET /users/42/orders/7/items/3 looks tidy. The mechanics are less tidy: nested resources create implied ownership semantics (who is allowed to fetch item 3 of order 7 of user 42?), and they couple client code to a hierarchy the server may later want to flatten.
Nesting communicates ownership and scoping, and it is correct when the parent is genuinely a mandatory part of the identity — a cart item has no meaning outside its cart. It is wrong when the nesting is cosmetic. GET /users/42/orders to mean "orders for user 42" can be expressed identically as GET /orders?user_id=42, and the flat form is strictly more flexible: it survives the day a user's orders are moved to a different subdomain, it composes with pagination and filtering, and it doesn't leak the hierarchy into every client's URL construction.
GET /users/42/orders?status=open
GET /orders?user_id=42&status=openGuidelines that hold up in practice: nest at most one level deep; treat the parent ID as part of the identity only when the resource cannot exist without the parent; and when a resource can be reached two ways, pick one canonical route and redirect or alias the other. Clients cache URLs — two URLs for the same thing is how stale-cache bugs are born.
Status code semantics: what the code actually promises
Status codes are the part of the contract that clients parse before they parse the body. That means they must be mechanically reliable — a client's retry loop, cache, and error handling all branch on them.
The core semantics that matter:
- 2xx — the request did what it said.
200 OKfor a completed read or write;201 Createdwith aLocationheader when a resource was created;202 Acceptedwhen the work happens later (it's the code that says "you will not be able to GET this result yet"). - 4xx — the client can fix this.
400for malformed syntax,404when the resource does not exist,409when the request conflicts with current state (and the body should say what state),422when the request is well-formed but semantically invalid. The distinction between 400 and 422 is real: retrying a 422 without changing the payload will never succeed. - 5xx — the server is at fault. This is the class that must be reserved for actual server failures, because retryable, cacheable, and circuit-breaker logic all key off it.
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/order-already-shipped",
"title": "Order already shipped",
"status": 409,
"detail": "Cannot cancel an order that has been shipped (shipped_at 2026-08-17T14:22:00Z)."
}The two failures most APIs commit: returning 200 OK with an error in the body (which breaks every client that branches on status, and silently corrupts caches), and returning 500 for client errors (which trips retry loops into hammering the API with un-retryable requests and pollutes your error budget).
Consistency beats cleverness
The long-term value of an API comes from clients being able to predict it: the same kinds of failures produce the same codes, resources are addressed the same way, and the error body always has the same shape. That consistency is more valuable than any single modeling choice. Whether you model cancellation as an action or a resource matters less than the fact that every client on the platform can rely on: 404 means it doesn't exist, 409 means state conflicts, and 5xx is never the client's fault.