skip to content

An HTTP API has grown paths like `/customers/7/orders/42/items/9001/discounts/3`. What problems does that depth create, and how would you restructure it?

level: juniorimportance: should knowfreq 50%

answer

  1. only the last segments carry identity
  2. reparenting breaks every deep URI
  3. verify every segment or the URI can lie
  4. one level of nesting, two max
  5. nest to list and create; canonical top-level for the item

basics

~20 s

Deep paths force clients to know an entire ancestry to address one thing, break when a resource is reparented, and duplicate handlers per level. Keep nesting to about one level of parent: address the deepest resource by its own top-level URI, /discounts/3, and use nesting only for scoped listing and creation.

solid answer

~50 s

Problems with `/customers/7/orders/42/items/9001/discounts/3`: - **Clients must carry the full ancestry** to build a URL for a resource they already have the id of. Store a link, not a path recipe. - **Fragility.** If the item ever moves to another order, every stored URI is wrong — even though the discount itself did not change. - **Redundancy.** Everything before `items/9001` is derivable from the item; the extra segments carry no information and must all be validated for consistency, or they become a lie. - **Handler sprawl.** Each level multiplies routes, and each route needs its own parent-mismatch checks. Restructure: cap nesting at roughly one parent level. `GET /order-items/9001/discounts` to list, `POST /order-items/9001/discounts` to create, and `GET|PATCH|DELETE /discounts/3` for the item. Cross-cutting queries go to the top-level collection with filters: `GET /discounts?orderId=42`. Put the relationships in the representation as links so clients navigate rather than assemble paths.

code

http · 12 lines
http
# instead of
GET /customers/7/orders/42/items/9001/discounts/3

# canonical item
GET /discounts/3

# scoped listing and creation
GET  /order-items/9001/discounts
POST /order-items/9001/discounts   -> 201, Location: /discounts/3

# cross-cutting query
GET /discounts?orderId=42

go deeper

for a junior

Say that deep paths are hard to build, break when things move, and that the item should have its own short URI.

for a middle

Add the consistency burden of validating every segment, route sprawl, and the list-and-create-nested / read-flat split.

for a senior

Discuss URI stability as a contract, the redirect-based migration, and where nesting is genuinely mandatory because ids are parent-scoped.

for a principal

Set the house rule and its enforcement: canonical URIs, a depth cap, links in representations, and how to deprecate the deep routes without stranding clients.

## Why depth accumulates Deep URIs usually come from mirroring the database's foreign-key chain, or from adding one level at a time as the domain grows: first `/orders/{id}`, then items under orders, then discounts under items. Each step looks harmless. The result is a path where only the last one or two segments carry identity and the rest is decoration. ## What deep nesting actually costs **Addressability.** A URI is supposed to be a stable name for a resource. If naming a discount requires knowing its item, its order, and its customer, then a client that has only the discount id cannot construct the URL. Real clients hold ids — from a webhook, a log line, a stored reference — and now they need extra lookups just to build a request. **Stability under change.** URIs should survive domain changes that do not change the resource. Move a line item to a different order (a merge, a correction, a split shipment) and every deep URI beneath it changes, invalidating caches, bookmarks, stored references, and anything that logged the URL. The discount did not change; only its ancestry did. **Redundancy and consistency burden.** In `/customers/7/orders/42/items/9001`, the customer and order are already implied by item 9001. That means the server must either verify every segment for consistency — returning `404` when item 9001 is not in order 42, or order 42 not under customer 7 — or ignore them, in which case the URI can assert something false and two different URIs address the same resource. Verifying costs extra lookups on every request; ignoring costs correctness. **Route and test sprawl.** Every level of nesting multiplies routes: list, create, read, update, delete at each depth, each needing its own authorization and mismatch handling. The same logic gets reimplemented per level and drifts. **Ugly practical limits.** Long paths bump into URL length limits in proxies and logs, make access logs unreadable, and make API-gateway route matching and rate-limit rules harder to write. ## The restructuring rule **One level of nesting is the norm; two is the maximum; three is a smell.** Concretely: - Every resource that has a globally unique id gets a **canonical top-level URI**: `/order-items/9001`, `/discounts/3`. - Nesting is used for the two operations where the parent genuinely supplies context: **listing** a parent's children (`GET /order-items/9001/discounts`) and **creating** one (`POST /order-items/9001/discounts`, responding `201` with `Location: /discounts/3`). - Cross-cutting reads use the top-level collection with filters: `GET /discounts?orderId=42&status=active`. - The representation carries **links**, so a client that has an order does not need to know the path grammar to reach its items — it follows the URL you gave it. If child ids are only unique within the parent, you cannot fully flatten — `/orders/42/items/1` must keep its parent. In that case cap at that one level and give the *grandchildren* their own global ids so the chain stops there. ## Migration Because URIs are a contract, flattening usually means running both shapes for a while: keep the deep routes but answer them with `301`/`308` redirects to the canonical short URI, emit only the short form in every response body and `Location` header, and remove the deep routes once traffic to them has decayed. Log the old routes so you can tell when that is.

  • You want to flatten these URIs but clients already use the deep ones. How do you migrate?
    Keep the deep routes answering, but return `301` or `308` to the canonical short URI, and stop emitting deep URIs anywhere in response bodies or `Location` headers. Instrument the old routes so you can see traffic decay, then remove them once it is negligible. In a pre-production system you would simply change both sides at once.
  • Is there a case where you genuinely cannot flatten?
    Yes — when the child's identifier is only unique within its parent, such as line number 1 of order 42. Then `/orders/42/items/1` is the shortest unambiguous name and one level of nesting is mandatory. The fix is to stop the chain there by giving deeper resources their own global identifiers.

Filing a document by its full corporate org chart path means every reorg invalidates the filing, even though the document never changed.

saying these in an interview costs you the question

  • Believing deep nesting is 'more RESTful' or required by REST
  • Ignoring intermediate path segments in the handler so the URI can assert a false relationship
  • Mirroring the foreign-key chain in the URL by default
  • Assuming URIs never need to survive a resource being reparented
  • Adding a new nesting level for every new relationship instead of a filter or a link

context