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; 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.
| Concept | What happens |
|---|
| File → URL | A file at src/routes/<kebab>.tsx maps to the URL /<kebab>. The path you pass to createFileRoute matches that location. |
| Root route | src/routes/__root.tsx is the app-wide layout and the error / not-found boundaries that wrap every page. |
| Index route | src/routes/index.tsx is the index (/) route — the landing page. |
| Generated tree | All route files are compiled into src/bootstrap/routeTree.gen.ts. This is tooling output — never edit it by hand. |
| Boundaries | Each route can declare pendingComponent, errorComponent, and notFoundComponent so loading, failure, and missing states render predictably. |
| Nesting | A layout renders its child routes through <Outlet /> from @tanstack/react-router. |
| Mounting | The router and React root are created and mounted in src/bootstrap/app.tsx, where global providers (e.g. the TanStack Query client) are wired. |
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:
import { createFileRoute } from "@tanstack/react-router";
import { SettingsErrorLayout } from "../features/settings/layouts/SettingsErrorLayout";
import { SettingsLayout } from "../features/settings/layouts/SettingsLayout";
import { SettingsNotFoundLayout } from "../features/settings/layouts/SettingsNotFoundLayout";
import { SettingsSkeletonLayout } from "../features/settings/layouts/SettingsSkeletonLayout";
export const Route = createFileRoute("/settings")({
component: SettingsLayout,
pendingComponent: SettingsSkeletonLayout,
errorComponent: SettingsErrorLayout,
notFoundComponent: SettingsNotFoundLayout,
});
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:
import { Outlet } from "@tanstack/react-router";
export const SettingsLayout = ({ children }: { children?: React.ReactNode }) => {
return <section>{children ?? <Outlet />}</section>;
};
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:
import { useSearch } from "@tanstack/react-router";
export const DEFAULT_LANG = "en";
// strict: false opts out of route-specific search typing so it works from any route.
export const useLang = (): string => {
const search = useSearch({ strict: false }) as { lang?: string };
return search.lang ?? DEFAULT_LANG;
};
Passing strict: false opts out of route-specific search typing, so the hook works regardless of which route the component is mounted under — 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.
Best practices
- Keep routes thin. A route file should wire a component and its boundaries — nothing more. Push UI into features and data fetching into hooks.
- Never edit
routeTree.gen.ts. It is generated from src/routes/ and Biome-ignored. Change routing by editing files in src/routes/, then let the tree regenerate.
- Put backend calls in hooks. Routes wire; hooks fetch. Keep data access out of route files so they stay declarative — see data fetching.
- Use the boundaries. Declare
pendingComponent, errorComponent, and notFoundComponent so loading, failure, and missing states are explicit instead of blank screens.
- Centralize app-wide concerns. Put shared layout and boundaries in
__root.tsx, and global providers in bootstrap/app.tsx, rather than repeating them per route.
- Read params through router hooks. Use
useSearch (and the router’s param hooks) instead of reading window.location directly, so values stay in sync with navigation.