React 19 is the runtime. Routing is still how users move. Most modern React stacks sell file-based routing as the default, drop a file, get a URL - and then bury the real differences in undocumented folder conventions.
I care about three axes: convention clarity, nested UI ownership, and escape hatches when the URL shape refuses to match the filesystem.
What "file-based" actually promises
A file-based router maps paths on disk to URL segments, usually with special filenames for layouts, loading UI, and dynamic params. The promise is:
- New routes are discoverable by browsing a folder
- Colocation of UI + data for that URL
- Less hand-maintained route config
The lie is that every framework means the same thing by those words.
The cast (mid-2025)
I am comparing conventions, not declaring a winner:
| System | Mental model | Dynamic / catch-all flavor |
|---|---|---|
| Next.js App Router | app/ tree; page, layout, loading, error, route | [id], [...slug], route groups (marketing) |
| Remix (flat / folder routes) | URL-ish filenames or folders; loaders/actions beside UI | $id, splats per Remix conventions |
| React Router framework mode | Similar loader/action story; file conventions via RR/Vite plugins | Param syntax per chosen convention |
| Astro | src/pages/ file = route; islands opt into interactivity | [id], rest params, content-first |
| TanStack Router (file mode) | Optional file generation into a typed route tree | Strong types; config still first-class |
April's framework comparison post picked on navigation/data/deploy. This post zooms into how the files teach the URL.
Axis 1: Convention clarity
Ask: can a new hire predict the URL from the path in under ten seconds?
Next App Router is powerful and dense. Route groups (shop) disappear from
the URL; parallel routes and intercepting routes add concepts you only need for
specific UX. Great when you live in that model daily. Costly when half the team
still thinks in pages/.
Remix / RR tend to keep filenames closer to URLs (invoices.$id.tsx reads
like /invoices/:id). That clarity is a feature for HTTP-shaped brains.
Astro is the clearest for content sites: blog/post.md → /blog/post. The
trade-off is that app-like nested authenticated chrome is not its center of
gravity.
Decision rule: if your product is mostly documents and marketing, prefer the boring pages directory. If your product is nested app chrome with independent loading states, prefer a router that treats layouts as first-class
- and budget time to learn its special files.
Axis 2: Nested UI ownership
File-based routing pays off when layouts nest and each segment can own loading and error UI.
textapp/ layout.tsx → shell invoices/ layout.tsx → invoices chrome page.tsx → /invoices [invoiceId]/ page.tsx → /invoices/:invoiceId loading.tsx → segment-level pending
That structure is the App Router pitch. Remix nested routes express the same idea with outlet composition. Either way, the failure mode is the same: a single giant layout that re-fetches the world on every child navigation because nobody drew segment boundaries.
tsx// app/invoices/layout.tsx, illustrative ownership boundary export default function InvoicesLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="invoices-shell"> <InvoicesNav /> <main>{children}</main> </div> ); }
If every child page re-implements the nav, you do not have nested routing, you have copy-paste with a fancy folder name.
Axis 3: Escape hatches
Pure convention breaks. You will need at least one of:
- Route groups / pathless layouts (organize without changing URLs)
- Optional / repeating params without inventing ghost folders
- Programmatic route trees when generated docs or multi-tenant paths explode
- Typed route APIs when stringly-typed
hrefs become a bug factory
ts// When files fight the product, illustrative escape // Prefer a typed route helper over sprinkling '/invoices/' + id everywhere export const href = { invoice: (id: string) => `/invoices/${id}` as const, invoiceEdit: (id: string) => `/invoices/${id}/edit` as const, };
If your app's URL space is a graph (wizards, modals-as-routes, multi-panel tools), file-based routing still works, but only if you admit config/helpers beside the files. Dogma that "everything must be a file" produces empty directories and magical filenames nobody can pronounce.
Decision criteria
| Situation | Lean |
|---|---|
| Content + docs + light interactivity | Astro pages (or Next app used calmly) |
| Nested app UI + loader/action mental model | Remix / React Router framework |
| RSC + platform features as default | Next App Router |
| Extreme type-safe navigation as the product need | TanStack Router (dedicated deep dive) |
| Tiny SPA, few routes | Explicit route config may beat folders |
Failure modes
Folder archaeology. Route groups, parallel routes, and intercepting routes stacked without a README, only the author can add a page.
URL and filesystem divergence nobody documented. "Why is this not the path?" becomes the onboarding ritual.
Layouts that own too much data. Nested routes with a root layout that blocks on the union of all child fetches.
Migrating pages/ → app/ as a moral crusade. Migrate by surface area that
benefits; dual trees are allowed while you learn.
Assuming file-based means zero config forever. Generated routes, redirects, and typed links always return.
Continuity
File conventions are the map. TanStack Router covers when type-safe routes are worth the overhead over framework defaults, and how that choice sits next to the React Router / Next / Remix landscape from April.
Takeaway
File-based routing is a clarity bet: predictable URLs, nested ownership, and documented escape hatches. Pick the convention your team can read at a glance - special filenames only help until nobody remembers what they mean.