TanStack Router is a type-safe router for React and Solid apps. It defines the entire architecture of a view directly within the route itself. The path, params, search state, loader inputs, pending state, and error boundary are defined before the component renders.
Setting up a TanStack Router app starts with one structural decision: how to define the route tree. That choice (file-based routes, code-based routes, or a mix) shapes where you declare paths, params, search schemas, loaders, and route context. Params and search validation define URL state; loaders and route context define the data boundary; layouts, guards, pending UI, and error UI define when child routes can render.
When the app moves to TanStack Start, those route definitions also describe server work. On Vercel, loaders run as Vercel Functions on Fluid compute for I/O-bound requests to databases, LLMs, and upstream APIs.
Link to headingFile routes and code routes
Both file and code routes produce the same typed route graph, so the decision is really about where route contracts live and who maintains them.
File-based routing fits teams that want the route tree to stay visible in the filesystem. A route file defines the path, component, loader, params, search schema, pending component, and error component in one place. The generated route tree provides TypeScript with enough information to infer valid paths and route APIs throughout the app.
import { createFileRoute } from '@tanstack/react-router'export const Route = createFileRoute('/projects/$projectId')({ component: ProjectPage, loader: async ({ params }) => { return fetchProject(params.projectId) },})function ProjectPage() { const project = Route.useLoaderData() return <h1>{project.name}</h1>}Code-based routing fits teams that want explicit route objects, especially in libraries, embedded apps, or route trees assembled from feature modules. After you define the root route and child routes, you add them to the tree yourself.
import { createRootRoute, createRoute, createRouter, Outlet,} from '@tanstack/react-router'const rootRoute = createRootRoute({ component: () => <Outlet />,})const projectsRoute = createRoute({ getParentRoute: () => rootRoute, path: '/projects/$projectId', component: ProjectPage,})const routeTree = rootRoute.addChildren([projectsRoute])export const router = createRouter({ routeTree })The tradeoff is mostly about workflow. File-based routes keep navigation easy to scan and maintain, but they introduce a generation step and a “routes live in folders” convention that the team needs to follow. Code-based routes avoid filesystem conventions and make composition explicit, but they push more setup (hierarchy, parenting, organization) into code that the team must maintain deliberately.
For React teams choosing TanStack Router over React Router, Remix, or the Next.js App Router, this specific architectural flexibility is often the deciding factor. While file-based conventions provide a fast baseline, the route graph can still be manually assembled in code when a feature module or embedded flow demands exact boundaries.
Link to headingType-safe route params
Path params are where router type safety first becomes visible. A project detail page at /projects/$projectId should not make every component rediscover what projectId exists, parse it by hand, or accept a misspelled param name until runtime.
In TanStack Router, a path param is part of the route definition. The loader receives it, the component can read it, and typed navigation requires it when linking to the route.
export const Route = createFileRoute('/teams/$teamId/reports/$reportId')({ loader: async ({ params }) => { return getReport({ teamId: params.teamId, reportId: params.reportId, }) }, component: ReportPage,})function ReportPage() { const { teamId, reportId } = Route.useParams() const report = Route.useLoaderData() return <ReportView teamId={teamId} reportId={reportId} report={report} />}React Router and the Next.js App Router both expose route params, and both generate route types through a codegen step. TanStack Router runs a comparable generation step for its route tree, but the payoff is broader: one inferred route graph ties together the path, param names, loader input, link targets, and component hooks, and typed navigation enforces params at the link site, so you cannot link to /teams/$teamId/reports/$reportId without supplying both IDs. When a route changes from /reports/$reportId to /teams/$teamId/reports/$reportId, TypeScript follows the change through the navigation surface.
In apps where route params are resource identifiers, a wrong account ID, workspace ID, or eval run ID can fetch the wrong data, invalidate the wrong cache entry, or show a valid UI for the wrong resource. Type safety still needs authorization checks, but it removes a class of wiring mistakes before the request leaves the browser.
Link to headingSearch params become app state
Search params arrive in the URL as flat strings, but an app relies on them as structured state: page numbers, booleans, tab state, filters, date ranges, and arrays of selected values. Parsing and validating raw input into a type the app can trust is where bugs tend to accumulate, because that translation is usually scattered across components by hand.
Link to headingHow TanStack Router models search params
TanStack Router's search params model parses search params into structured JSON, then lets each route validate that raw URL input into a type the app can trust. The route owns the boundary between user-editable URL text and application state.
type IssueSearch = { page: number status: 'open' | 'closed' assignee: string | undefined}export const Route = createFileRoute('/issues')({ validateSearch: (search): IssueSearch => { let page = 1 if (typeof search.page === 'string') { page = Number(search.page) } let status: IssueSearch['status'] = 'open' if (search.status === 'closed') { status = 'closed' } let assignee: string | undefined if (typeof search.assignee === 'string') { assignee = search.assignee } return { page, status, assignee, } }, loaderDeps: ({ search }) => ({ page: search.page, status: search.status, assignee: search.assignee, }), loader: async ({ deps }) => { return fetchIssues(deps) },})Two route options make that split explicit.
validateSearchturns raw URL input into typed state with defaults.loaderDepstells the router which validated search fields should be used to key the loader result.
loaderDeps ensures the loader result is keyed to your validated filters, so filter changes refetch the right data. If two /issues URLs differ by page or status, the router needs to know that they are different data states. loaderDeps makes that relationship explicit, so changing a filter invalidates the right loader result instead of forcing the component to coordinate URL state and cache state by hand.
This contrasts with the client-hook pattern, where the component mounts, reads search state through a hook like useSearchParams(), and reconciles it with cache state by hand. Next.js Server Component pages and React Router framework-mode loaders also receive search params before render; what TanStack Router adds at that boundary is validation, typing, and loader keying, so the route holds validated search state before the component renders.
Link to headingLoaders belong to routes
Route loaders turn TanStack Router from a simple URL matcher into the application's data layer. A loader receives route input and context before returning the typed data a component consumes.
In TanStack Router, data loading is one connected system: loaderDeps, stale-while-revalidate caching, pending components, and error handling all attach to the same route. They work best together because they define a single thing: when a route is ready to render.
export const Route = createFileRoute('/runs/$runId')({ loader: async ({ params, context, abortController }) => { return context.api.getRun({ runId: params.runId, signal: abortController.signal, }) }, component: RunPage,})function RunPage() { const run = Route.useLoaderData() return <RunTimeline run={run} />}For simple screens, this pattern keeps fetch logic near the page that needs it. For larger apps, route context lets the loader depend on shared services without importing global singletons into every route.
import { createRootRouteWithContext } from '@tanstack/react-router'type RouterContext = { api: { getRun(input: { runId: string; signal: AbortSignal }): Promise<Run> }}const rootRoute = createRootRouteWithContext<RouterContext>()({ component: AppShell,})When a loader calls a database, model provider, or slow enterprise API, that call becomes part of the route contract, so its loading and error states belong at the same boundary.
Link to headingPending UI stays local
Loading and error states work best when they are scoped to the part of the UI that is waiting or broken. TanStack Router lets routes define pendingComponent and errorComponent, so a slow loader does not need to blank the whole app shell.
import { useRouter } from '@tanstack/react-router'export const Route = createFileRoute('/reports/$reportId')({ loader: async ({ params }) => getReport(params.reportId), pendingComponent: ReportSkeleton, errorComponent: ({ error }) => { const router = useRouter() return <ReportError error={error} onRetry={() => router.invalidate()} /> }, component: ReportPage,})By default, TanStack Router waits before showing a pending component, which helps avoid flashes for loaders that resolve quickly. For data-heavy apps, teams can tune pending thresholds per route or globally, then keep skeletons close to the loader boundary.
This route-level model mirrors a broader pattern in modern app routers:
Remix, now merged into React Router v7, renders the nearest nested error boundary when a loader, action, or component fails.
The Next.js App Router uses segment files such as
loading.tsxanderror.tsxaround nested routes.SolidStart pairs async route data with
SuspenseandErrorBoundary.
Across these routers, loading and failure depend on whether the route is waiting or broken.
Link to headingLayouts define route boundaries
Nested layouts provide the structure that makes route-level data work. A layout route defines the shared shell, context, and parent data for a subtree, and each child route renders into the outlet with its own loader data.
In TanStack Router, layouts can be pathless, which means they define structure without adding a URL segment. That is useful for authenticated app shells, settings areas, or product surfaces that share navigation but expose many child URLs.
export const Route = createFileRoute('/_app')({ component: AppLayout,})function AppLayout() { return ( <Shell> <Outlet /> </Shell> )}In TanStack Router, a route boundary is both a UI boundary and a data boundary. A parent layout fetches shared account context once, and child routes inherit it as they load their own page-specific records, so data fetching follows the same hierarchy as the UI.
If every page component fetches its own data and layouts only render chrome, the app rebuilds routing by hand with effects, stores, and provider state.
Link to headingProtecting nested routes
Auth checks should happen before protected child routes load. TanStack Router's authenticated route pattern is used beforeLoad for that job, which keeps private pages from flashing before a redirect.
beforeLoad runs before a route and its children load, which makes it the natural place for auth checks. It receives the same route context inputs as loaders, and it can throw a redirect when the user lacks access.
import { createFileRoute, redirect } from '@tanstack/react-router'export const Route = createFileRoute('/_authenticated')({ beforeLoad: async ({ context, location }) => { const session = await context.auth.getSession() if (!session) { throw redirect({ to: '/login', search: { redirect: location.href, }, }) } return { session } }, component: AuthenticatedLayout,})A guard's position in the tree sets its scope: a guard on a parent route protects every child route below it, and if the guard fails, the children do not attempt to load. That prevents wasted data requests and keeps unauthorized screens from briefly rendering while the app discovers that the user should not be there.
Route guards also make permission checks easier to review. Instead of hunting through page components for auth effects, teams can inspect the route tree to see session requirements, workspace-role checks, and public surfaces in a single hierarchy. When the app scales to the full-stack TanStack Start framework, these route guards execute securely on the server during the initial render, acting as the first line of defense to protect the page experience before your backend middleware takes over.
Link to headingTanStack Start completes the stack
TanStack Router is the routing layer. TanStack Start on Vercel turns that routing model into a full-stack React framework with SSR, streaming, server functions, bundling, and Nitro-based deployment.
If the app is a client-heavy SPA, TanStack Router can sit inside a Vite React app. If the app needs server rendering, server functions, and full-document streaming, TanStack Start adds a framework layer without sacrificing TanStack Router's typed route graph.
The type-safe loader pattern also fits a larger framework trend. SvelteKit uses route-level load functions with generated $types for page and layout data, while SolidStart uses route preloading, query, and createAsync to connect route data with Suspense and error boundaries. Nuxt uses route-aware async data composables. Different ecosystems made different API choices, but they are all solving the same problem. The router should know enough about data to coordinate rendering.
React Router framework mode and Remix make route modules the full-stack unit, while the Next.js App Router builds around filesystem segments and React Server Components. TanStack Router starts with typed route state; paired with TanStack Start, that route graph becomes the full-stack model.
Link to headingVercel runs every React stack
The choice of router should determine how your app is structured. Vercel supports Next.js, SvelteKit, SolidStart, Nuxt, TanStack Start, Astro, Remix, Vite, React Router, and other frameworks, so a team choosing TanStack Router stays inside the Vercel deployment model.
On Vercel, TanStack Start apps use Vercel Functions and Fluid compute by default, meaning route loaders and server functions run on infrastructure optimized for server-rendered and I/O-bound work. For loaders that call LLMs, databases, vector stores, or slow upstream APIs, Vercel Functions limits give Node.js and Python functions with Fluid compute a 300-second default max duration, with Pro and Enterprise projects configurable up to 800 seconds.
Waiting on I/O, including AI model calls and database queries, does not count toward active CPU time. For AI- and data-heavy apps, whose route loaders spend most of their time waiting on I/O, this keeps costs tied to actual compute rather than wall-clock time.
Vercel's product surface stays the same when the React stack changes:
Fluid compute runs route loaders, server functions, and SSR work close to the app's data needs.
AI SDK provides React teams with a TypeScript toolkit for streaming AI interfaces and model calls.
AI Gateway gives AI loaders one endpoint for model access, retries, fallback behavior, and observability.
Vercel Sandbox isolates untrusted or model-generated code when routes power agentic workflows.
Vercel Workflows handles durable work that should not be performed within a single navigation request.
Because TanStack Start loaders and server functions deploy as Vercel Functions automatically, the route contracts you write locally in TanStack Router are the same units that run in production. There is no separate runtime to configure before the app ships. When you’re ready to ship TanStack Router with TanStack Start, deploy on Vercel.
Link to headingTanStack Router FAQ
What is TanStack Router?
TanStack Router is a type-safe router for React and Solid apps. It supports route definitions, nested layouts, typed params, route loaders, route context, pending UI, error components, and SSR patterns.
When should a React team choose TanStack Router?
TanStack Router is well-suited to apps where the URL serves as the single source of truth for the interface. Dashboards, admin tools, internal tools, search-heavy interfaces, and AI product surfaces benefit from typed params, validated search state, loader dependencies, and route-level UI states.
How is TanStack Router different from React Router?
React Router's framework mode provides type-safe route modules, loaders, and actions, as well as code-splitting, SSR, SSG, and SPA strategies. TanStack Router takes a different approach, where typed route state and inferred loader data sit at the center of the router API.
How is TanStack Router different from the Next.js App Router?
The Next.js App Router uses folders and files to define route structure. React Server Components are part of that model. TanStack Router works with React and Solid, and TanStack Start adds a full-stack layer for teams that want SSR, streaming, and server functions.
Can TanStack Router deploy on Vercel?
TanStack Router can be deployed to Vercel as a standard Vite React app, while TanStack Start deploys to Vercel via its Nitro configuration. TanStack Start apps on Vercel use Vercel Functions and Fluid compute by default.
Does TanStack Router replace TanStack Query?
TanStack Router does not replace TanStack Query. TanStack Router has route loaders and SWR-style caching, while TanStack Query remains a strong fit for rich client-side server-state management. A common pattern is to use route loaders for the foundational data required to render a view, while leaning on TanStack Query to manage complex mutations, background polling, and highly interactive client-side state.