skip to content

When a static folder and a dynamic segment at the same depth both match a URL, which one serves the request, and what decides that?

level: middleimportance: should knowfreq 62%

answer

  1. a tree has no first and last
  2. structure decides, not traversal order
  3. literal beats dynamic beats catch-all
  4. compared left to right, first difference wins
  5. equal specificity is a conflict to fix

basics

~20 s

The more specific shape wins: a literal segment beats a dynamic one, which beats a catch-all, compared depth by depth from the left. A directory tree has no declaration order, so routers rank by specificity instead.

solid answer

~50 s

A file tree is a set, not a sequence — there is no "declared first" and directory read order varies by filesystem — so a file-based router cannot resolve overlaps the way an order-based router does. Instead it **sorts candidate routes by specificity**, comparing segment by segment from the left: a **literal** segment outranks a **dynamic** one, a dynamic segment outranks a **catch-all**, and an optional catch-all ranks last because it matches the most. So a literal folder and a dynamic sibling at the same depth both match, and the literal one serves the request. The payoff: a broad catch-all cannot shadow a route added later, a real failure mode where the first matching registration wins. The cost: two shapes of equal specificity at one depth are genuinely ambiguous, and most routers report that as a conflict rather than choosing silently.

go deeper

for a junior

Recall the ranking: an exact name beats a variable segment, which beats a catch-all. Nothing about the order in which files were created or read affects which page a URL gets.

for a middle

Explain why order is unavailable at all — a tree is a set and listing order varies by machine — and walk the left-to-right specificity comparison that replaces it.

for a senior

Show how you debug a wrong-page report with the ladder rather than by shuffling files, and how you stop a broad catch-all from turning genuine not-founds into rendered pages.

for a principal

Weigh determinism against expressiveness: structural precedence removes a whole class of ordering bugs, but it also means the URL space must be expressed in tree shape, and conflicts must be an error the pipeline enforces.

## Why order cannot decide In a router where you register routes by calling something, the calls happen in an order, and that order is available as a tiebreaker: first match wins, or last wins. A file-based router has no such sequence. The routes come from a **directory listing**, and listing order is an artefact of the filesystem — it differs between machines, between operating systems, and between a clean checkout and an incremental build. A router that resolved overlaps by traversal order would serve different pages on a developer laptop and in the build container, which is why file-based routers resolve them **structurally** instead. ## The specificity ladder The ranking that nearly every file-based router implements, from most to least specific: 1. A **literal** segment — the folder or file name is the URL text. 2. A **dynamic** segment — one variable piece. 3. A **catch-all** — one or more remaining pieces. 4. An **optional catch-all** — zero or more remaining pieces, the broadest shape there is. The comparison runs **segment by segment from the left**, and the first position where two candidates differ decides the winner; later positions never rescue a candidate that lost earlier. So a route that is literal at depth one and dynamic at depth two beats a route that is dynamic at depth one however literal it becomes further down. | Both match `/products/featured` | Rank | Result | |---|---|---| | literal `products/featured` | most specific | serves the request | | `products/` + dynamic segment | next | serves every other product id | | `products/` + catch-all | broadest | serves deeper tails only | ## What this buys you The headline property is **order independence**: adding a new literal route under a folder that already has a catch-all is safe, because the new route is strictly more specific and therefore wins wherever it matches. In an order-based router the same addition can be invisible if a broad pattern was registered earlier, and the fix there is to move the registration. Here there is nothing to move. A second property is **stability**: the same tree produces the same table everywhere, so a routing difference between two environments points at a difference in files, not in traversal. ## Where it still bites - **Equal specificity is ambiguity.** Two dynamic siblings at the same depth — two variable folders under one parent — match exactly the same URLs with different parameter names. Neither is more specific, so most routers report a conflict; the ones that fall back to a name-ordering rule are picking arbitrarily and you should not rely on it. - **A shadow can still be created upward.** Specificity is compared from the left, so a literal segment high in the tree can claim a whole subtree away from a dynamic sibling, and the route you expected to run is never even considered. - **A catch-all absorbs typos.** It does not shadow more specific routes, but it does claim every path nobody else claimed, so URLs that should have been not-found arrive at your route instead. A catch-all must therefore decide, in code, when a tail is unresolvable and produce the not-found outcome itself. - **Adding literals to defeat a catch-all does not scale.** If you find yourself adding literal folders to carve exceptions out of a broad tail, the URL space probably wants a different shape rather than a growing list of special cases. ## How to reason about a surprise When the wrong page renders for a URL, the question is never "which route was registered first" — it is **which candidate is more specific at the first position where they differ**. The debugging sequence that follows from that: 1. Write out every on-disk shape that could match the URL, left to right. 2. Compare them position by position until one wins; that is the route the router picked. 3. If two of them tie all the way down, you have a genuine conflict and the fix is in the tree, not in the code. Frameworks do differ at the edges — whether an optional segment ranks above or below a catch-all of the same breadth, whether flat file names and folder chains can produce an overlap at all, whether a conflict is a build error or a warning. The ladder itself, and the fact that structure rather than order decides, is the portable part.

  • Why is a build-time conflict better than a router silently picking one of two equally specific routes?
    Because a silent pick is unstable and undiscoverable: it depends on a rule nobody remembers, can differ between framework versions, and leaves a route that never runs and never explains why. A conflict surfaces the ambiguity while the tree is still cheap to change.
  • Does specificity ranking mean a catch-all is harmless?
    No. It cannot shadow more specific routes, but it still claims every path nobody else claimed, so mistyped and retired URLs land in it instead of the not-found path. A catch-all owns the job of deciding when a tail is unresolvable and producing a not-found outcome itself.
  • A literal route high in the tree seems to steal URLs from a dynamic sibling. Is that a bug?
    It is the ladder working as defined: comparison runs left to right, so a literal at the first differing position wins the whole subtree below it. If those URLs really belong to the dynamic branch, the literal name is the wrong shape — rename it or move it deeper.

Sorting mail by how exactly it is addressed: a named person beats a flat number, which beats 'anyone at this street'. The order the letters came out of the bag never matters.

saying these in an interview costs you the question

  • Says the route declared or read first wins
  • Thinks moving a folder changes which route matches
  • Believes a catch-all shadows more specific sibling routes
  • Compares whole paths instead of segment by segment
  • Assumes two dynamic siblings at one depth resolve sensibly
  • Treats a catch-all as a substitute for not-found handling