Packages
TanStack Start relies on three core TanStack packages that work together to provide file-based routing, nested layouts, SSR and full-stack app features.
@tanstack/react-start: The full-stack framework layer. Handles SSR, server functions, document rendering and app entry setup.@tanstack/react-router: Powers all routing - file-based routes, nested layouts, loaders, path params, search params and navigation.@tanstack/router-plugin: Build-time Vite plugin that scanssrc/routes/and generates therouteTree.gen.tstype file.
Project Structure
The src/routes directory defines the app’s route tree, use file based routing
src/
├── components # Reusable React components
├── hooks # Custom React hooks
├── lib # Utility functions, DB code and business logic
├── routes/
│ ├── posts/
│ │ ├── route.tsx # Directory route for /posts; parent layout for child routes
│ │ ├── index.tsx # Index route for /posts
│ │ └── $slug.tsx # Dynamic route for /posts/:slug
│ ├── _account/
│ │ ├── route.tsx # Pathless layout route; wraps children without adding /account
│ │ ├── orders.tsx # Route for /orders
│ │ └── profile.tsx # Route for /profile
│ ├── __root.tsx # Root route; wraps the entire app
│ ├── index.tsx # Index route for /
│ ├── about.tsx # Route for /about
│ ├── robots[.]txt.ts # File route for /robots.txt
│ └── $.tsx # Catch-all / splat route
├── global.css # Global styles imported by __root.tsx, e.g. shadcn tokens
├── router.tsx # Router instance and app router setup
└── routeTree.gen.ts # Generated route tree; do not edit manuallyNavigation
| API | Use for |
|---|---|
<Link> | Normal in-app navigation; preferred for anything clickable |
useNavigate() | Imperative navigation after a side effect |
<Navigate> | Immediate client-side redirect on render |
router.navigate() | Imperative navigation outside React components |
useMatchRoute() / <MatchRoute> | Checking whether a route is currently active or pending |
<Link>
Use <Link> for most navigation. It supports route params, search params, active state and preloading.
import { Link } from "@tanstack/react-router";
<Link
to="/posts/$slug"
params={{ slug: "my-post" }}
search={{ page: 2 }} // (optional) query string values like ?page=2
preload="intent" // (optional) preload route data/code on hover or touch
activeProps={{ className: "font-semibold" }} // (optional) props applied when the link is active
activeOptions={{ exact: true }} // (optional) only mark active on an exact path match
>
My Link Text
</Link>;useNavigate()
Use for redirects after actions (like mutations).
import { useNavigate } from "@tanstack/react-router";
const navigate = useNavigate();
navigate({
to: "/posts/$slug",
params: { slug: "new-post" },
search: { page: 2 }, // (optional) query string values like ?page=2
hash: "comments", // (optional) navigate to a hash fragment like #comments
});Route Matching
Use route matching to check whether a route is currently active or pending.
<MatchRoute>
Use <MatchRoute> for conditional rendering
import { MatchRoute } from "@tanstack/react-router";
function Nav() {
return <MatchRoute to="/posts">Posts page is active</MatchRoute>;
}useMatchRoute()
Returns a function that checks whether a route matches the current location.
import { useMatchRoute } from "@tanstack/react-router";
function Sidebar() {
const matchRoute = useMatchRoute();
const isPostsActive = !!matchRoute({
to: "/posts",
});
return <div>{isPostsActive ? "Posts is active" : "Posts is inactive"}</div>;
}Params
Path Params
Use path params for dynamic URL segments like /posts/:slug.
Path params can be accessed in a route loader or inside the route component with Route.useParams().
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/posts/$slug")({
component: PostPage,
// Access path params inside the loader
loader: async ({ params }) => {
return fetchPost(params.slug);
},
});
function PostPage() {
// Access path params inside the component
const { slug } = Route.useParams();
return <div>Post: {slug}</div>;
}Search Params
Use search params for query strings like ?page=2&sort=newest. Define validateSearch on the route to parse and type URL values, then read them with Route.useSearch(). Update them with <Link search={...}> or navigate({ search: ... }). You can pass a Zod schema directly to validateSearch since Zod schemas expose a .parse method.
import { createFileRoute, Link } from "@tanstack/react-router";
import { z } from "zod";
const searchSchema = z.object({
page: z.number().catch(1),
sort: z.enum(["newest", "oldest"]).catch("newest"),
});
export const Route = createFileRoute("/posts/")({
component: PostsPage,
validateSearch: searchSchema,
loaderDeps: ({ search }) => search, // re-run loader when search params change
loader: async ({ deps: { page, sort } }) => ({
posts: await fetchPosts({ page, sort }),
}),
});
function PostsPage() {
const { page, sort } = Route.useSearch();
const { posts } = Route.useLoader();
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{/* Use the function form to avoid clobbering other params */}
<Link from={Route.fullPath} search={(prev) => ({ ...prev, page: prev.page + 1 })}>
Next page
</Link>
</div>
);
}Zod Adapter
If you need search params to be optional when navigating (i.e. <Link to="/posts/"> without a search prop) while still having correct types and fallback values, the plain Zod approach won't get you there cleanly:
.default()— makes navigation requiresearchprops.catch()— makes navigation optional but causes type loss (unknown)
The @tanstack/zod-adapter package solves this with a fallback helper that retains types while allowing optional navigation:
import { fallback, zodValidator } from "@tanstack/zod-adapter";
const searchSchema = z.object({
page: fallback(z.number(), 1).default(1),
sort: fallback(z.enum(["newest", "oldest"]), "newest").default("newest"),
});
export const Route = createFileRoute("/posts/")({
validateSearch: zodValidator(searchSchema),
// ...
});For most apps the plain Zod approach is fine. Reach for the adapter when you have routes where search params should always be optional.
Data Loading (loaders)
Use loaders to fetch data for route components.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/invoice/$id")({
component: InvoicePage,
// Loaders can access path params and search params
loader: async ({ params }) => ({
invoice: await fetchInvoice(params.id),
user: await fetchCurrentUser(),
}),
});
function InvoicePage() {
// Access loader data with Route.useLoader()
const { invoice, user } = Route.useLoader();
return (
<div>
Invoice for ${invoice.amount} by {user.name}
</div>
);
}Server Functions
Define server functions in the src/lib directory to handle server-side logic like database reads/writes, authentication or third-party API calls.
import { createServerFn } from "@tanstack/react-start";
// GET request (default)
export const listInvoices = createServerFn().handler(async () => {
return await db.invoice.findMany();
});
// POST request
export const updateInvoice = createServerFn({ method: "POST" })
// Use Zod for input validation
.inputValidator(z.object({ id: z.number(), amount: z.number() }))
.handler(async ({ data }) => {
const { id, amount } = data;
return await db.invoice.update({ where: { id }, data: { amount } });
});Call server functions from the loader or using the useServerFn hook inside route components.
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { useServerFn } from "@tanstack/react-start";
import { listInvoices, updateInvoice } from "@/lib/invoices";
export const Route = createFileRoute("/invoices")({
component: InvoicesPage,
loader: async () => ({
// Call within a loader
invoices: await listInvoices(),
}),
});
function InvoicesPage() {
const router = useRouter();
const { invoices } = Route.useLoader();
// Call with useServerFn inside a component
const updateInvoiceFn = useServerFn(updateInvoice);
const handleUpdate = async (id: number) => {
await updateInvoiceFn({ id, amount: 100 });
await router.invalidate(); // re-run loaders to reflect updated data
};
return (
<div>
{invoices.map((invoice) => (
<div key={invoice.id}>
Invoice #{invoice.id}: ${invoice.amount}
<button onClick={() => handleUpdate(invoice.id)}>Update</button>
</div>
))}
</div>
);
}Static Server Functions
Static Server Functions are executed at build time, which is useful for Static Site Generation (SSG).
bun add -D @tanstack/start-static-server-functionsimport { createServerFn } from "@tanstack/react-start";
import { staticFunctionMiddleware } from "@tanstack/start-static-server-functions";
const listPosts = createServerFn({ method: "GET" })
.middleware([staticFunctionMiddleware])
.handler(async () => {
return db.post.findMany();
});Server Routes
Use server routes to handle HTTP requests directly from a route file. Useful for webhooks, health checks, or any endpoint that doesn't need a UI.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/health")({
server: {
handlers: {
GET: async ({ request }) => {
return new Response("OK");
},
},
},
});