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?
answer
- a tree has no first and last
- structure decides, not traversal order
- literal beats dynamic beats catch-all
- compared left to right, first difference wins
- equal specificity is a conflict to fix
basics
~20 sThe 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 sA 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
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.
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.
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.
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