Resources and URL Design: Nouns, Plurals, Hierarchy
What This Concept Is
A resource is the noun the API exposes. A URL names an individual resource or a collection of resources. REST is built on the idea that the URL points at a thing (not an action), and the HTTP verb decides what to do with the thing.
Baseline rules that most large APIs converge on:
- resources are nouns, not verbs:
/orders, not/createOrder - collections are plural:
/users, not/user - individual items are addressed by identifier:
/users/u_123 - hierarchy reflects containment:
/projects/p_1/reports/r_99means "reportr_99in projectp_1" - URLs are stable - identifiers do not change when fields change
- URL segments are
lowercase-kebab-caseorlowercase; nevermixedCase
Resources can be top-level or nested. Nested resources imply that the child cannot exist without the parent. Cross-cutting resources stay top-level.
Why It Matters Here
URL structure is the first thing a consumer sees and the hardest thing to change. A URL leaks through logs, bookmarks, CDNs, OpenAPI specs, client SDKs, and routing tables. A poorly chosen hierarchy propagates for years.
Resource modeling is also where most failure modes live. Endpoints that feel natural from the implementation side (/saveUser, /processOrderBatch2) read as noise from the consumer side.
Concrete Example
A project-management API modeled two ways.
Bad (actions-as-endpoints):
POST /createProject
POST /addMemberToProject
POST /listReportsOfProject
POST /deleteProjectReport
GET /project?id=p_1
Good (resources + verbs):
GET /projects # list
POST /projects # create
GET /projects/{projectId} # read
PATCH /projects/{projectId} # partial update
DELETE /projects/{projectId} # delete
GET /projects/{projectId}/members # list members (scoped)
POST /projects/{projectId}/members # add a member
DELETE /projects/{projectId}/members/{userId}
GET /projects/{projectId}/reports
POST /projects/{projectId}/reports
GET /projects/{projectId}/reports/{reportId}
Notice:
- verbs disappear from URLs; they live in HTTP methods
- collection / item distinction is uniform:
/Xfor list and create,/X/{id}for read/update/delete - hierarchy only where a child logically belongs to a parent
projectIdis passed positionally, not in query string
When to flatten. Hierarchies deeper than two levels start to hurt. Instead of /orgs/{o}/projects/{p}/reports/{r}/comments/{c}, prefer a top-level comments resource with a report_id reference:
GET /comments?report_id=r_99
GET /comments/{commentId}
Geewax is explicit: resources are entities, and deep hierarchies are an anti-pattern because they prevent reuse (you cannot easily list comments across reports, for example).
A full resource representation example:
{
"id": "proj_01HXABC",
"name": "Q4 Planning",
"created_at": "2026-11-01T10:00:00Z",
"owner": { "id": "u_17", "href": "/users/u_17" },
"status": "active"
}
href lets clients navigate without reassembling URLs themselves. That is HATEOAS in its most useful, minimal form.
Common Confusion / Misconception
"I need a new URL for this action." Usually you need a new verb, a new field, or a custom method (Cluster 3). A new URL per action ends up as /doThing, /doThingFast, /doThingFastV2.
"Nested resources are always better than flat." They are worse when the child has a meaningful cross-parent existence. A report belonging to a project is naturally nested. A comment that can reference many parent types should be flat.
"Resources must map 1:1 to database tables." No. Resources should match the consumer's mental model. A single resource may span tables; a single table may be invisible to consumers. The contract is decoupled from storage.
"GET with a body is fine for complex queries." It is not - many proxies, CDNs, and HTTP libraries strip or ignore bodies on GET. For a read with a complex filter, use query parameters or a POST /things:search custom method (Cluster 3).
How To Use It
When modeling a new API:
- List the nouns in the domain (often one-to-one with DDD aggregates).
- Pick which are top-level resources and which are children.
- Draw the hierarchy; if any branch is deeper than two levels, flatten it.
- Name collections plural, items by ID, everything lowercase.
- For each resource, design the five standard methods before any custom action (Cluster 2's next concept).
- Only after that, look at what does not fit and consider custom methods (Cluster 3).
Write the full URL list on one page before writing any request/response schemas.
Check Yourself
- Is
/users/{id}/avatara sub-resource or a field? How would you decide? - Your API has
/reports/{rid}/comments/{cid}and/tasks/{tid}/comments/{cid}. What is the cost to consumers who want "all comments I authored"? How would you fix it? - A teammate proposes
/api/getOrdersByUser/{uid}. Rewrite it as a resource URL and explain the change.
Mini Drill or Application
Design the URL surface for this domain: an online library with patron, book, copy, loan, reservation, and fine. Produce:
- the list of top-level resources with justification
- the hierarchy where sensible, flattened otherwise
- the full URL list (collections and items) for the five standard methods per resource
- a one-paragraph note on any deliberate deviations (e.g., why
/copiesis top-level even though copies belong to books)
Constrain to one page. If it does not fit, the model is too deep.