REST Resource Modeling: How to Design URLs That Make Sense
REST APIs organize their surface around resources — the nouns of your domain. How you identify, name, and structure those resources determines whether your API feels intuitive or requires constant documentation reference. Good URL design is not aesthetic preference. It is communication: URLs tell developers what the API contains, how it is organized, and how to navigate it. Done well, a developer can infer what endpoints exist from the ones they already know.
Resources Are Nouns, Not Verbs
The first principle of REST URL design is that URLs identify resources — things — not operations. Operations are expressed through HTTP methods. The resource is the noun; the method supplies the verb.
This is correct:
POST /orders
GET /orders/456
PUT /orders/456
DELETE /orders/456
This is wrong:
POST /createOrder
GET /getOrder?id=456
POST /updateOrder
POST /deleteOrder
The verb-in-URL pattern is RPC-style thinking mapped onto HTTP URLs. It works technically but loses the structural clarity of REST: there is no predictable way to infer what operations exist, no consistent pattern for how resources relate, and no benefit from the HTTP method vocabulary.
If you find yourself wanting to put a verb in a URL, ask what the resource is. POST /createOrder becomes POST /orders. POST /cancelOrder/456 becomes POST /orders/456/cancellations or PATCH /orders/456 with a status field. The verb belongs in the method or the request body, not the URL.
Naming Conventions
Use plural nouns for resource collections:
/users
/orders
/products
/invoices
Plural is the near-universal convention for REST APIs and the one developers expect. A collection of users is /users, not /user. Individual resources within the collection are addressed by ID:
/users/123
/orders/456
Use lowercase letters and hyphens for multi-word resource names, not camelCase or underscores:
/payment-methods # correct
/paymentMethods # avoid
/payment_methods # acceptable but inconsistent with hyphen convention
Hyphens are the URL convention (RFC 3986 treats hyphens as safe characters; underscores require encoding in some contexts). Lowercase avoids case-sensitivity issues — URLs are technically case-sensitive, but most developers expect them to work regardless of case.
Keep resource names short and domain-obvious. /payment-methods is better than /stored-payment-instruments. /orders is better than /purchase-transactions. The canonical name for a concept in your domain is the right name for the resource.
Hierarchy and Nesting
Nested resources express a parent-child relationship where the child cannot exist independently of the parent:
/users/123/orders # orders belonging to user 123
/orders/456/line-items # line items belonging to order 456
/organizations/789/members # members of organization 789
Nesting communicates the relationship and provides natural scoping. A request to /users/123/orders clearly asks for orders belonging to user 123, not all orders. The hierarchy is meaningful, not arbitrary.
Limit nesting depth. Two levels is usually the maximum before URLs become unwieldy:
/users/123/orders/456/line-items # three levels, borderline
/users/123/orders/456/line-items/789/components # too deep
When resources are legitimately three or four levels deep, consider whether you actually need the full path for scoping, or whether a flatter structure with query parameters serves as well:
GET /line-items?order_id=456 # flat with filter
GET /orders/456/line-items # nested
Both are valid; the nested form is more expressive of the relationship, but the flat form is simpler and scales better when query patterns are complex.
Actions That Do Not Map Cleanly to CRUD
Not all operations map naturally to create/read/update/delete. Canceling an order, publishing a draft, verifying an email address — these are state transitions or actions that feel awkward expressed as resource manipulation.
One approach is to treat the action as a sub-resource:
POST /orders/456/cancellations # cancel order 456
POST /articles/789/publications # publish article 789
POST /email-verifications # initiate email verification
This preserves the noun-oriented structure while expressing actions clearly.
Another approach is to accept that PATCH on the parent resource is appropriate when the action is fundamentally a state change:
PATCH /orders/456
{"status": "cancelled"}
Which to use depends on whether the action creates a distinct resource (a cancellation record that has its own identity and attributes, like a timestamp and a reason), or whether it simply mutates the parent’s state. If a cancellation has its own data, model it as a resource. If it is purely a status change, PATCH the parent.
Query Parameters for Filtering, Sorting, and Search
Query parameters are for modifying how a collection is returned, not for identifying the resource itself:
GET /orders?status=pending&sort=created_at&order=desc&limit=20
GET /[email protected]
GET /products?category=electronics&min_price=100&max_price=500
Filtering, sorting, pagination, and search are all appropriate uses of query parameters. The base URL identifies the resource collection; query parameters shape the response.
Be consistent in how you name query parameters across the API. If sorting uses sort and order on one endpoint, it should use those same names on every endpoint that supports sorting. If pagination uses limit and cursor, those names should be consistent throughout. Parameter inconsistency is one of the most common API DX failures.
IDs in URLs
Use opaque identifiers in URLs rather than sequential integers where possible. Sequential integers reveal the total count of a resource (your one-millionth user knows they are user 1,000,000), allow competitors to estimate your scale, and facilitate enumeration attacks where someone iterates IDs to discover unauthorized resources.
UUID or prefixed IDs (like Stripe’s cus_abc123 format) do not carry this information and are easier to identify in logs (you can tell a customer ID from an order ID from a payment ID at a glance). Stripe’s prefixed ID format is increasingly the pattern of choice for public APIs.
For internal APIs where enumeration is not a concern and readability matters, sequential IDs are simpler and have no material downside.
Predictability as the Goal
A well-designed URL structure is predictable. A developer who knows /users/123 exists can infer that /users/123/orders might exist. A developer who knows GET /products returns a collection can infer that POST /products creates a new product. A developer who has used one endpoint well can predict how the others behave.
Predictability comes from applying conventions consistently across the entire API surface. Every exception to the convention is a place where prediction fails and documentation becomes necessary. Minimize exceptions, document the ones that exist, and design each new resource by asking what a developer who has used the rest of the API would expect. Their expectation is usually the right answer.