Skip to main content
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.
ConceptWhat happens
File → URLA file at src/routes/<kebab>.tsx maps to the URL /<kebab>. The path you pass to createFileRoute matches that location.
Root routesrc/routes/__root.tsx is the app-wide layout and the error / not-found boundaries that wrap every page.
Index routesrc/routes/index.tsx is the index (/) route — the landing page.
Generated treeAll route files are compiled into src/bootstrap/routeTree.gen.ts. This is tooling output — never edit it by hand.
BoundariesEach route can declare pendingComponent, errorComponent, and notFoundComponent so loading, failure, and missing states render predictably.
NestingA layout renders its child routes through <Outlet /> from @tanstack/react-router.
MountingThe 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.
  • SPA overview — what the Single Page App is and how it fits together.
  • Structure — where routes/, features/, and bootstrap/ live.
  • Features — the components and layouts a route delegates to.
  • Data fetching — the hooks that load data for a route.
  • Internationalization — the useLang hook and language handling.
  • Backend routing — server-side routing, distinct from this client-side router.