TanStack Start
Reference for TanStack Start, a full-stack React framework powered by TanStack Router. Start adds full-document SSR, streaming, server functions, server routes, middleware, and full-stack builds on top of Router; Router itself still owns routing, loaders, search params, and navigation.
Routing belongs to the sibling skill — see tanstack-router for file-based routes, loaders, search-param validation, and navigation. This skill covers only what Start adds on top. For server-state caching inside components, see tanstack-query.
Start is at Release Candidate stage: the API is considered stable and feature-complete, but not bug-free.
Branch-specific references, loaded on demand:
- server-functions.md —
createServerFn, validators, serialization, errors, redirects, streaming, static server functions. - middleware.md — request vs function middleware, context
passing, global middleware in
src/start.ts, CSRF. - server-routes.md — HTTP endpoints defined with the
serverproperty on file routes. - rendering.md — selective SSR, SPA mode, static prerendering, and ISR-style cache headers.
- deployment.md — hosting targets, environment variables, and build outputs.
First Checks
- Confirm
@tanstack/react-startand@tanstack/react-routerare installed, and which build tool the project uses — Vite (@tanstack/react-start/plugin/vite) or Rsbuild (@tanstack/react-start/plugin/rsbuild). The plugin import path and several config shapes differ between them. - Read the build config (
vite.config.ts/rsbuild.config.ts) before changing anything:prerender,spa,pages,serverFns, andserver.buildall live in thetanstackStart()options. - Look for
src/start.ts. If it exists, Start does not auto-install the CSRF middleware — that file owns global request and function middleware. - Find
src/router.tsx(router creation) andsrc/routes/__root.tsx(the document shell withHeadContentandScripts); both are required. - Check whether a host adapter plugin is already wired in (Cloudflare, Netlify, Nitro) — plugin order in the array matters.
Project Setup
Scaffold with npx @tanstack/cli@latest create, or clone an official example
with npx gitpick TanStack/router/tree/main/examples/react/start-basic.
Minimal dependency set: @tanstack/react-start, @tanstack/react-router,
react, react-dom, plus the build tool (vite + @vitejs/plugin-react, or
@rsbuild/core + @rsbuild/plugin-react).
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
server: { port: 3000 },
plugins: [
tanstackStart(),
// react's vite plugin must come after start's vite plugin
viteReact(),
],
})
Rules that bite:
- The React plugin must come after
tanstackStart()in the Vite plugin array. package.jsonneeds"type": "module".- In
tsconfig.json, keepverbatimModuleSyntaxdisabled — enabling it can leak server bundles into client bundles. Use"jsx": "react-jsx","moduleResolution": "Bundler","module": "ESNext",strictNullChecks. src/routeTree.gen.tsis generated on first run; do not hand-edit it.
Two files are required beyond the config:
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
return createRouter({ routeTree, scrollRestoration: true })
}
// src/routes/__root.tsx
import {
createRootRoute,
HeadContent,
Outlet,
Scripts,
} from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [{ charSet: 'utf-8' }, { title: 'App' }],
}),
component: () => (
<html>
<head>
<HeadContent />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
),
})
HeadContent and Scripts are not optional: without them, head tags and the
client bundle never reach the document.
Server Functions vs Server Routes
Both run on the server; they answer different questions.
| Need | Use | Why |
|---|---|---|
| Call server logic from your own app (loaders, components, handlers) | createServerFn |
Start handles serialization and typing across the boundary. |
| An HTTP endpoint called from outside the app (webhooks, third parties, OAuth callbacks) | Server route (server.handlers on a file route) |
Raw Request/Response, no Start client required. |
Server functions are same-origin RPC endpoints protected by CSRF checks; they are not a public API surface. Server routes are.
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
const CreateUser = z.object({ name: z.string().min(1) })
export const createUser = createServerFn({ method: 'POST' })
.validator(CreateUser)
.handler(async ({ data }) => {
return db.users.create(data)
})
Details in server-functions.md and server-routes.md.
Where Code Runs
Route loader and beforeLoad are isomorphic: they run on the server for
the initial request and on the client for subsequent navigations. Reading
process.env.SECRET in a loader exposes it to the client bundle. Put
server-only work behind a server function.
import {
createServerOnlyFn,
createClientOnlyFn,
createIsomorphicFn,
} from '@tanstack/react-start'
const getSecret = createServerOnlyFn(() => process.env.API_SECRET)
const saveLocal = createClientOnlyFn((v: string) => localStorage.setItem('k', v))
const log = createIsomorphicFn()
.server((m: string) => console.log(`[SERVER] ${m}`))
.client((m: string) => console.log(`[CLIENT] ${m}`))
Server functions can be statically imported anywhere, including client
components — the build replaces the implementation with an RPC stub. Avoid
await import() of server-function modules; dynamic imports break that
transformation.
Rendering Strategy
Every route matching the initial request is server-rendered by default. Narrow
it per route with ssr:
| Value | beforeLoad / loader on server |
Component SSR |
|---|---|---|
true (default) |
yes | yes |
'data-only' |
yes | no |
false |
no | no |
Change the app-wide default with defaultSsr in createStart. Inheritance is
one-way: a child can only make the inherited value more restrictive, so
ssr: true under a parent with ssr: false has no effect.
For a fully client-rendered app, use SPA mode (spa: { enabled: true }), which
prerenders a shell to /_shell.html. See rendering.md.
Middleware
createMiddleware() produces request middleware, which runs for every server
request including SSR and server functions.
createMiddleware({ type: 'function' }) produces server-function middleware,
which additionally supports .client() and .validator().
import { createMiddleware } from '@tanstack/react-start'
export const authMiddleware = createMiddleware().server(
async ({ next, request }) => {
const session = await auth.getSession({ headers: request.headers })
if (!session) throw new Error('Unauthorized')
return next({ context: { session } })
},
)
Always call next(); the chain stops otherwise. Attach auth middleware to
every server function that reads or writes private data — a route beforeLoad
guard is UX, not the data boundary, because server functions are reachable
independently of the UI that calls them. Full model in
middleware.md.
Environment Variables
Start loads .env, .env.development, .env.production, and .env.local
automatically.
- Server code reads any variable from
process.env. - Client code only sees the build tool's public prefix:
VITE_for Vite,PUBLIC_for Rsbuild, viaimport.meta.env. - Read
process.envinside handlers and middleware, not at module scope. On Cloudflare Workers and similar edge runtimes, env is injected per request, so module-scope reads evaluate toundefined. - To get a runtime (not build-time) value to the client, return it from a server function and pass it through a loader.
More, including typing and staticNodeEnv, in deployment.md.
Review Checklist
- Build config is coherent:
tanstackStart()before the React plugin (Vite),"type": "module",verbatimModuleSyntaxdisabled. __root.tsxrendersHeadContentandScripts;routeTree.gen.tsis generated, not hand-edited.- Every server function has a
.validator()when it takes input, and the validated value is checked for authorization, not just shape — a well-formed id is not an authorized id. - Auth middleware is attached to the server functions and server routes that
touch private data, not only to route
beforeLoad. src/start.ts, when present, installscreateCsrfMiddleware()explicitly.- No secret is read at module scope, and no secret sits behind a
VITE_orPUBLIC_prefix. - Loaders contain no server-only assumptions; server work goes through
createServerFnorcreateServerOnlyFn. Cache-Controlon anything identity-dependent isprivateorno-store, neverpublic(rendering.md).- Routes needing browser APIs set
ssr: falseor'data-only'and provide apendingComponentfallback. - SPA-mode and static-host redirects allow
/_serverFn/*and server-route paths through to the server (rendering.md). - Deployment target matches the build output and start script (deployment.md).