REST principles and resource-oriented URLs
coreintermediateREST organises an API around *things* rather than *actions*. A URL names a resource — `/orders/`, `/orders/57/`, `/orders/57/items/` — and the HTTP method says what to do with it. That is why `POST /orders/create/` is redundant and `POST /orders/` is not: the verb is already in the method. The other principles that matter day to day are statelessness (every request carries everything needed to serve it — no server-side conversation between calls) and a uniform interface (the same method means the same thing on every resource, so a client that understands one endpoint understands the rest). Collections are plural, identifiers go in the path, and anything that only *narrows* a collection goes in the query string.
Think of it as
The test for a good REST URL is whether you can read it out loud as a noun phrase. `/orders/57/items/` is "the items of order 57" — a thing that exists. `/getOrderItems?orderId=57` is a function call spelled with slashes; it works, but the client now has to learn a vocabulary of function names instead of a shape. That distinction is not aesthetic: a noun-shaped URL space is enumerable and cacheable, and a router can generate it, which is precisely why DRF ships routers and not an RPC dispatcher. The place people get stuck is the operation that genuinely is not CRUD — refund an order, publish an article, retry a job. Two honest answers exist, and inventing a verb in the collection URL is neither of them: either the action *is* a resource in its own right (`POST /orders/57/refunds/` creates a refund, which is a real thing with an id and a history), or it is a sub-action on the resource (`POST /orders/57/refund/`, DRF's `@action`). Prefer the first when the operation produces something you would want to list later; it turns "what happened to this order" from a log grep into a `GET`.
What we're doing: Model a refund as a sub-resource so it has its own identity, listing, and history — rather than as a verb.
- 4–5
- Nesting one level expresses containment: a refund belongs to exactly one order and has no meaning without it. Going deeper would produce URLs clients get wrong.
- 10–12
- Only create and list — a refund is not editable or deletable, and leaving those mixins out is how the URL space states that rather than a comment.
- 17
- Scoping to the parent id from `kwargs` is what makes the nesting real: `/orders/57/refunds/` cannot return refunds belonging to order 58.
- 19–21
- The order and the issuing user are set server-side in `perform_create()`, never accepted from the request body — the same rule as any server-controlled field.
Why this works: Modelling the operation as a resource gives it an id, a timestamp, an author, and a list endpoint for free. "Refund this order" as a verb would have produced the same database row and no way to ask what refunds exist.
Putting an action in the collection URL
Wrong
Better
What you see: Routing conflicts appear first: `/orders/refund/` and `/orders/{pk}/` overlap, so a real order whose lookup value is `refund` — a slug, a reference code — becomes unreachable. Then the authorization gap: the object id arrives in the body, so `get_object()` and `has_object_permission()` never run.
Why: A collection URL addresses the whole collection, so anything appended to it is read by the router as an identifier. Beyond the routing collision, moving the id from the path into the body takes it out of the path where every permission, filter and object lookup expects it — which is why the second form is not just tidier but is the one where the framework's own object-level checks still apply.
- RPC-shaped — the verb is in the URL
- Every operation is a new name the client must be told about.
- The method carries no meaning — everything is POST.
- Nothing is cacheable, because a GET and a mutation look alike.
- No router can generate it; every path is hand-written.
- Adding an operation means adding vocabulary, not reusing a shape.
- Resource-oriented — the verb is the method
- One noun, five methods — a client that learns the shape once reuses it everywhere.
- GET is safe and cacheable; DELETE and PUT are idempotent. The method says so.
- A DRF router generates the whole URL set from one ViewSet.
- Non-CRUD operations become sub-resources with their own history.
- New resources cost no new vocabulary.
The same five operations, RPC-shaped and resource-shaped
Together
Remember: The URL names a thing and the method says what to do with it, so `POST /orders/create/` states the verb twice. Collections are plural, ids go in the path, query parameters only narrow. Nest one level to express containment and no more. A non-CRUD operation is either its own sub-resource — `POST /orders/57/refunds/`, which gains an id and a list endpoint — or a named sub-action, never a verb hanging off the collection URL, which collides with the detail route and moves the id out of the path where object permissions live.
See also: http semantics and status codes · versioning backward compatibility and deprecation · viewsets and routers · versioned and named conventions

