Engineering Notes
Home/Backend/API Design/Resource Design
Backend · API Design

Resource Design

Good REST APIs are designed around resources, not actions. Compare RPC-style and resource-style endpoints, decide how deep to nest, model actions that don't fit CRUD, and pick conventions you'll apply everywhere.

⚙️ Backend● Beginner⏱ 6 min read▶ 2 interactive🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Nouns, not verbs
  • Collections vs single items
  • Nesting and relationships
  • Non-CRUD actions, casing and pluralization

1Nouns, Not Verbs

The single most important rule of REST resource design: URIs name things, HTTP methods name actions. The verb belongs in the method, not the path.

text
BAD:   POST /createOrder
BAD:   GET  /getOrderById?id=482
GOOD:  POST /orders            (create an order)
GOOD:  GET  /orders/482        (read order 482)

If you find yourself putting a verb in a URL, it's a sign the operation should be expressed as a different HTTP method on a resource, or (see section 4) modeled as its own sub-resource.

2Collections vs. Single Items

Every resource type gets two URI shapes:

ShapeExampleRepresents
Collection/ordersThe set of all orders (GET = list, POST = create one)
Item/orders/482One specific order (GET = read, PUT/PATCH = update, DELETE = remove)

Collections are (almost) always plural nouns — /orders, not /order. This stays consistent whether the collection has 0, 1, or 10,000 members.

3Nesting and Relationships

When a resource only makes sense in the context of a parent, nest it:

text
GET  /orders/482/items          -> items belonging to order 482
GET  /orders/482/items/9        -> item 9 of order 482
POST /orders/482/items          -> add an item to order 482

Depth guidance: keep nesting to one or two levels. /customers/7/orders/482/items/9/reviews/3 is technically consistent but painful to work with. A common pattern once you're deep enough is to "promote" the inner resource to a top-level collection reachable by its own id, and only use nesting for creation/listing in context:

text
GET /items/9              -> works regardless of which order it came from
GET /orders/482/items      -> list items scoped to order 482 (nesting used only for "in-context" listing)

Embedding vs. linking

A collection response can either embed related data inline or link to it and require a follow-up request:

json
// embedded — one request, larger payload, can go stale relative to the source of truth
{ "id": 482, "customer": { "id": 7, "name": "Ana" } }

// linked — smaller payload, always fresh, costs an extra request
{ "id": 482, "customer": { "href": "/customers/7" } }

Neither is "more correct" — it's a trade-off between request count and payload size/freshness, often solved with an opt-in ?expand=customer query parameter that lets the client choose per-request.

4Actions That Don't Fit CRUD

Not every operation maps cleanly to create/read/update/delete. "Cancel this order," "reset this user's password," "publish this article" are actions, not resource replacements. Two accepted patterns:

Controller sub-resource (most common, recommended default):

text
POST /orders/482/cancel
POST /users/7/password-reset
POST /articles/55/publish

This models the action as a POST to a URI that reads like "the cancellation of order 482" — technically still a noun (the act of cancelling), and it composes cleanly with HATEOAS links (section 5 of REST Fundamentals).

State-changing PATCH (works when the action is really just a field transition):

text
PATCH /orders/482
{ "status": "cancelled" }

This works well when the action is simply "change this one field," but breaks down once the action needs side effects beyond setting a field (e.g. cancelling also needs to trigger a refund) — at that point the controller sub-resource pattern communicates intent more clearly.

5Casing and Pluralization Conventions

ConventionExampleNotes
lowercase, hyphenated segments/order-itemsMost common for multi-word resource names; avoids case-sensitivity bugs across systems
plural collection names/orders, /customersConsistent even for a collection that only ever has one row (/settings is a common, deliberate exception)
no trailing slash/orders not /orders/Pick one and be consistent — most frameworks treat these as distinct routes
identifiers own their own segment/orders/482 not /orders?id=482The path identifies which resource; query strings are reserved for filtering/pagination/sorting (its own topic)
Resource hierarchy tree for a food-delivery API showing collection and item nodes
Resource hierarchy tree for a food-delivery API showing collection and item nodes
Under the HoodHow a Router Actually Matches a Path to a Resource▾

Resource design isn't just a naming convention — it's compiled into real matching logic by the framework serving the API. Understanding that mechanism explains why certain design choices (ambiguous nesting, overlapping patterns) cause real bugs.

Path templates, not strings

A route like /orders/:id/items/:itemId (Express-style) or /orders/{id}/items/{itemId} (OpenAPI/RFC 6570 URI Template-style) is compiled — usually at server startup — into a matcher, most commonly a regular expression or a trie (prefix tree) keyed by path segment:

text
Route table (compiled once, matched per request):
  /orders                  -> regex ^\/orders\/?$
  /orders/:id               -> regex ^\/orders\/([^/]+)\/?$
  /orders/:id/items          -> regex ^\/orders\/([^/]+)\/items\/?$
  /orders/:id/items/:itemId   -> regex ^\/orders\/([^/]+)\/items\/([^/]+)\/?$

At request time, the incoming path /orders/482/items/9 is tested against each compiled pattern (or walked through the trie one segment at a time) until one matches; the captured groups (482, 9) are extracted into req.params.

Specificity ordering resolves ambiguity

Two patterns can both match the same literal path — e.g. a static route /orders/summary and a dynamic route /orders/:id both match /orders/summary. Frameworks resolve this by registration order (Express: first-registered-wins) or by an explicit specificity score (some routers rank literal segments above dynamic ones automatically, regardless of registration order). This is precisely why resource design guides tell you to register static sub-paths (/orders/summary, /orders/export) before the generic /orders/:id pattern — if :id is checked first, a request for /orders/summary gets incorrectly captured as id = "summary".

Trie-based routers scale better than a sequential regex scan

High-throughput frameworks (e.g. Fastify's find-my-way) build a radix trie over route segments instead of testing every route in registration order. Matching becomes a walk of depth equal to the number of path segments (O(segments)) rather than a linear scan over every registered route (O(routes)) — this is why deeply nested but well-structured resource hierarchies (section 3's guidance to cap nesting at 1-2 levels) matter for router performance at scale, not just for readability.

⚠️ Common Pitfalls

  1. Verbs in the URL — /getOrder, /orders/482/delete — the method already carries the verb; repeating it in the path is redundant and inconsistent once mixed with proper resource paths.
  2. Inconsistent pluralization — mixing /order/482 and /orders/999 across the same API confuses consumers and complicates client SDK generation.
  3. Over-nesting — /customers/7/orders/482/items/9/reviews/3/comments/1 — walk it back; promote deeply nested resources to top-level, scoped by query parameter or a single level of nesting for creation.
  4. Registering a dynamic route before a static one that overlaps it — causes the static route to be silently shadowed (section 6).
  5. Using query parameters for identity (/orders?id=482) instead of the path (/orders/482) — breaks caching (the path is the cache key for many HTTP caches) and REST's "resource identified by URI" constraint.
  6. Modeling every action as a PATCH field flip when the action has side effects beyond that one field — use a controller sub-resource instead (section 4).

📋 Quick Reference

text
Collection: plural noun            /orders
Item:       collection + id        /orders/482
Nested:     parent/id/child        /orders/482/items
Action:     controller sub-resource /orders/482/cancel   (POST)
Casing:     lowercase, hyphenated multi-word segments
Identity:   in the path, never in the query string
Nesting depth: cap at 1-2 levels; promote deep resources to top-level

🧠 Key takeaways

  • Paths name resources (plural nouns); methods are the verbs.
  • /orders = collection, /orders/{id} = item; nest at most one level for ownership.
  • Model complex actions as sub-resources or event nouns (/cancellations), never unsafe GETs.
  • Pick conventions (plural, kebab-case paths, one JSON casing) and enforce them with a linter.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How do you name REST resources?core▾

Plural nouns, lowercase with hyphens, hierarchical where there's real ownership: /users/{id}/orders, /orders/{id}/items. No verbs in paths (the method is the verb), and avoid deep nesting beyond two levels.

Follow-ups interviewers ask
  • /getUsers vs /users? /users with GET.
Q2How do you model actions that aren't CRUD?core▾

As sub-resources or state transitions: POST /orders/{id}/cancel, POST /payments/{id}/refunds (creating a refund resource), or by changing a field with PATCH (status=cancelled) when it's a simple transition.

Follow-ups interviewers ask
  • Which is preferable for refunds? Creating a refund resource, since it has its own data and lifecycle.
Q3How should related resources be returned?core▾

Return IDs or links by default, and allow expansion (?include=customer or ?expand=items) to avoid over-fetching and N+1 calls from clients. Keep payloads consistent across endpoints.

Follow-ups interviewers ask
  • Risk of always embedding? Large payloads and tight coupling to internal models.
Q4Should resource IDs be sequential integers?senior▾

Externally, prefer non-guessable IDs (UUIDs, prefixed IDs like ord_3fK…) to avoid enumeration and leaking business volume. Internally, integers are fine. Always enforce authorization regardless.

Follow-ups interviewers ask
  • Why prefixed IDs? They make IDs self-describing in logs and support (Stripe style).
Q5Should your API resources mirror database tables?core▾

No. Resources are designed for clients' use cases and can aggregate or hide tables. Exposing tables directly couples clients to your schema and leaks internals.

Follow-ups interviewers ask
  • How do you decouple them? DTOs/response models mapped from domain entities.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. Best path to cancel an order with business rules and an audit trail?
Action as a sub-resource.
2. Which is resource-oriented?
Collection + filter.
3. Tasks have globally unique IDs. Fetch one with…
Avoid needless nesting.
4. Most important naming rule?
Consistency.