The Single Page App routes with file-based TanStack Router. Every route is a file in src/routes/: its location on disk maps to a URL path, and the tooling compiles those files into src/bootstrap/routeTree.gen.ts, the generated route tree the router consumes at runtime. You write small, declarative route files, and the framework wires up the tree, type-safe navigation, and matching for you.
Routes stay thin. A route file is about wiring: it points a URL at a component and a set of boundaries (pending, error, not-found), and nothing more. The actual UI lives in features and data fetching lives in hooks, so each route reads as a clear map from path to behavior.
How it works
A route file declares a Route with createFileRoute. The path string mirrors the file’s location under src/routes/. The build step scans src/routes/, assembles every route into routeTree.gen.ts, and the router (mounted in src/bootstrap/app.tsx) uses that tree to match the current URL and render the matching component, or one of its boundaries while the route loads, errors, or fails to match.
Never edit src/bootstrap/routeTree.gen.ts by hand. It is generated from the files in src/routes/ and is Biome-ignored — any manual change is overwritten the next time the tree is regenerated. To change routing, add, rename, or remove files in src/routes/.
The root route
src/routes/__root.tsx is the entry of the tree. It defines the layout that wraps every page: shared chrome (navigation, providers-aware structure) plus the app-wide error and not-found boundaries. Every other route renders inside it, so put cross-page concerns here rather than repeating them per route.
The index route
src/routes/index.tsx is the index route for /, the landing page rendered when no deeper path matches. It is an ordinary route file: a Route declared with createFileRoute that points at the landing component.
Defining a route
Create a feature route with spa:feature:create. It generates a file at src/routes/<kebab>.tsx that maps to /<kebab>, declares the Route with createFileRoute, and wires a component alongside its pending, error, and not-found boundary components. A real generated route:
Each option maps to a state the router renders:
component — the page rendered when the route matches and is ready.
pendingComponent — shown while the route resolves (e.g. a skeleton).
errorComponent — shown when the route throws.
notFoundComponent — shown when no matching child is found.
The components come from the route’s feature; the route file only references them, keeping the wiring separate from the UI.
Nesting with Outlet
A layout renders its child routes through <Outlet /> from @tanstack/react-router. Place Outlet where children should appear, and the matched child route renders into that slot:
This is how a parent route wraps its children with shared structure while each child controls its own content.
Mounting the router
The router and the React root are created and mounted in src/bootstrap/app.tsx. The router is built from the generated routeTree.gen.ts, and global providers (such as the TanStack Query client) are wired around it there. That single file is the boundary between the generated route tree and the running app: add app-wide providers here, not in individual routes.
Reading query params
Navigation and links come from @tanstack/react-router. To read the current URL’s search parameters, use the useSearch hook. The spa’s useLang hook reads the lang query param from any route:
Passing strict: false opts out of route-specific search typing, so the hook works regardless of which route the component is mounted under. That is useful for cross-cutting values like the active language (see internationalization). Route params follow the same idea: read them through the router’s hooks rather than parsing the URL yourself.
Patterns to follow
A route file should wire a component and its boundaries and nothing more, so push UI into features and data fetching into hooks. Never edit routeTree.gen.ts: it is generated from src/routes/ and Biome-ignored, so change routing by editing files in src/routes/ and let the tree regenerate. Keep data access out of route files (routes wire, hooks fetch) so they stay declarative; see data fetching.
Declare pendingComponent, errorComponent, and notFoundComponent so loading, failure, and missing states are explicit instead of blank screens. Put shared layout and boundaries in __root.tsx, and global providers in bootstrap/app.tsx, rather than repeating them per route. Read params through useSearch and the router’s param hooks instead of reading window.location directly, so values stay in sync with navigation.