# TanStack Start

You are working in a TanStack Start project with React 19, TanStack Router, Vite and TypeScript. Write code for the versions in `package.json`, not the ones in your training data. Start is not Next.js or Remix: route loaders run on the server and in the browser, server-only code goes through `createServerFn`, and route types are inferred from a generated tree.

## Before you write code

- Read `package.json` for `@tanstack/react-start`, `@tanstack/react-router`, `@tanstack/react-query` and `zod`. Start and Router ship agent docs in `node_modules`: `@tanstack/start-client-core/skills/` (server functions, middleware, execution model, server routes, deployment) and `@tanstack/router-core/skills/` (search params, data loading, auth, navigation). Read the one for your task before using an API you are unsure of.
- `vite.config.ts` lists `tanstackStart()` from `@tanstack/react-start/plugin/vite` before the React plugin. It generates the route tree; don't add the standalone router plugin as well.
- `src/router.tsx` exports `getRouter()`; `src/routes/__root.tsx` renders the document; `src/routeTree.gen.ts` is generated (never edit it: it also registers your router's types); optional `src/start.ts` holds global middleware.
- Leave `verbatimModuleSyntax` off in `tsconfig.json`: with it on, server code can leak into client bundles.

## Routes

- File routes live in `src/routes`: `posts.tsx`, `posts/$postId.tsx`, `posts/index.tsx`, pathless layouts `_authed.tsx` with children in `_authed/`, groups `(marketing)/`, splats `$.tsx`. Files and folders starting with `-` are ignored, for colocated components.
- Each file exports `export const Route = createFileRoute("/posts/$postId")({ … })`. The generator keeps the path string in sync with the file; don't edit it by hand.
- Order route options as they infer: `validateSearch`, `loaderDeps`, `beforeLoad`, `loader`, `head`, then `component`.
- Types flow from the tree: never annotate or cast what `Route.useSearch()`, `Route.useParams()` or `Route.useLoaderData()` return. In other files, use `getRouteApi("/posts/$postId")` rather than importing `Route`, and don't export components from route files: both break code splitting.
- Link with `<Link to="/posts/$postId" params={{ postId }}>`, never by interpolating into `to`. Update search with a function (`search={(prev) => ({ ...prev, page: 2 })}`) so other params survive; navigate in code with `useNavigate({ from: Route.fullPath })`.
- Missing records `throw notFound()` and render the route's `notFoundComponent` (set a default on the root). Error components retry with `router.invalidate()`, which reruns the loader; `reset()` alone doesn't.

## Search params

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";

const productSearch = z.object({
  page: z.number().int().min(1).default(1).catch(1),
  sort: z.enum(["newest", "price"]).default("newest").catch("newest"),
});

export const Route = createFileRoute("/products")({
  validateSearch: productSearch,
  loaderDeps: ({ search }) => ({ page: search.page, sort: search.sort }),
  loader: ({ deps }) => listProducts({ data: deps }), // a server function
  component: ProductsPage,
});
```

- Zod 4 schemas are passed straight to `validateSearch`; `.default()` with `.catch()` keeps a bad URL from throwing. The `zodValidator` and `fallback` adapter is only for Zod 3.
- `loaderDeps` returns only the params the loader uses, never the whole `search` object. Store dates as ISO strings.

## Loaders, context and auth

- Loaders are isomorphic: they run on the server during SSR and in the browser on navigation. Database queries, secrets and file access never go in a loader or `beforeLoad`; call a server function. Never `fetch("/api/…")` with a relative URL from a loader.
- `beforeLoad` runs before loaders, parents first. Return an object to add to the context, or `throw redirect({ to: "/login", search: { redirect: location.href } })` with the `location` it receives.
- Put shared services in router context with `createRootRouteWithContext<{ queryClient: QueryClient }>()({ … })` (note the double call) and supply them in `getRouter()`.
- Auth: the root `beforeLoad` calls a `getSession` server function and puts the user in context; an `_authed.tsx` layout redirects anonymous users. That is UX only: every server function and server route that touches private data checks the session again.
- Return slow data as an unawaited promise and render it with `<Await promise={…} fallback={…}>` or a suspense query, so SSR streams it.
- Route data is stale at once by default (`staleTime: 0`). Set `staleTime` per route or `defaultStaleTime` on the router for data that changes rarely; show `pendingComponent` for slow routes.
- After a mutation, `await router.invalidate()` so loaders read the saved result.

## Server functions

```ts
// src/server/posts.functions.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { authMiddleware } from "./auth-middleware";

export const renamePost = createServerFn({ method: "POST" })
  .middleware([authMiddleware])
  .validator(z.object({ id: z.string(), title: z.string().trim().min(1).max(200) }))
  .handler(async ({ data, context }) => {
    return renamePostFor(context.user.id, data); // your data layer, checks ownership
  });
```

- Use `GET` for reads and `POST` for writes. Chain `.middleware()`, then `.validator()`, then `.handler()`; call it as `renamePost({ data: { id, title } })`. Recent versions name the validator method `validator` and deprecate `inputValidator`; use whichever the installed types mark current.
- Every server function is a public endpoint: validate its input and authorize inside it or its middleware, whatever route calls it.
- In components, wrap a server function with `useServerFn` when it can `throw redirect()` or `notFound()`, so the router handles the throw.
- Keep server functions in `*.functions.ts` files (safe to import anywhere, the client gets an RPC stub), server-only helpers in `*.server.ts`, and shared schemas in plain files. Import them statically.
- Inside handlers, read and write the HTTP layer with `getRequest`, `getRequestHeader`, `getCookie`, `setCookie`, `setResponseHeaders` and `setResponseStatus` from `@tanstack/react-start/server`. A response that depends on the session is never `Cache-Control: public`.
- Mark code that must not run on the other side with `createServerOnlyFn`, `createClientOnlyFn` or `createIsomorphicFn`; render browser-only UI in `<ClientOnly>` or after `useHydrated()` is true.

## Middleware and server routes

- `createMiddleware({ type: "function" })` wraps server functions (`.client()` and `.server()` phases); `createMiddleware()` is request middleware for every request: SSR, server routes and server functions. Pass data down with `next({ context: { user } })`.
- Register global middleware in `src/start.ts`: `export const startInstance = createStart(() => ({ requestMiddleware: [...], functionMiddleware: [...] }))`.
- Context sent from the client with `sendContext` is untrusted input: validate it and check it against the session.
- Session cookies are `HttpOnly`, `Secure` and `SameSite=Lax`. Check `Origin` on non-GET requests in request middleware, or use the CSRF middleware your version exports.
- Server routes add a `server` option to a file route, for webhooks, feeds, files and outside clients, never for your own pages' data:

```ts
export const Route = createFileRoute("/api/webhooks/billing")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        await handleBillingEvent(await request.text()); // verify the signature first
        return new Response(null, { status: 204 });
      },
    },
  },
});
```

- Add `middleware` under `server` for every handler, or use `handlers: ({ createHandlers }) => createHandlers({ … })` to give one method its own.

## SSR, head and TanStack Query

- The root route renders `<html>`, `<head><HeadContent /></head>` and a body ending in `<Scripts />`; without `Scripts` nothing hydrates.
- Set tags with the `head` option: `head: ({ loaderData }) => ({ meta: [{ title: loaderData?.post.title }, { name: "description", content: … }] })` (`loaderData` is optional there). Children override their parents' tags; add JSON-LD through `scripts`.
- Turn SSR down per route with `ssr: false` or `ssr: "data-only"` (children can only be stricter), or for the app with `defaultSsr` in `createStart`. Prerender static pages with `tanstackStart({ prerender: { enabled: true, crawlLinks: true } })`.
- Render nothing that differs between server and client (`Date.now()`, `Math.random()`, `window`) outside `useHydrated` or `<ClientOnly>`.
- With TanStack Query, create the `QueryClient` inside `getRouter()` (one per request), put it in router context, and connect them with `setupRouterSsrQueryIntegration({ router, queryClient })` from `@tanstack/react-router-ssr-query`. Loaders call `context.queryClient.ensureQueryData(postQuery(id))`, components `useSuspenseQuery(postQuery(id))`, and query functions call server functions. Set `defaultPreloadStaleTime: 0` so Router doesn't cache over Query, and invalidate queries after mutations.

## Environment and deployment

- Read `process.env` inside handlers and middleware, never at module scope: module scope can reach the client bundle, and edge runtimes inject env per request.
- Only `VITE_` variables reach the browser, through `import.meta.env`. Never give a secret that prefix.
- Hosting is a Vite plugin beside `tanstackStart()`: Nitro for Node, Vercel, Bun and others, the Cloudflare plugin for Workers (with `nodejs_compat`), Netlify's for Netlify. Keep the project's choice; see the deployment skill before adding one.

## Do not

- Do not write `"use server"` directives, `getServerSideProps`, `app/layout.tsx`, Remix `loader` or `action` exports, or imports from `next/*` or `react-router-dom`.
- Do not use pre-1.0 Start APIs: the `@tanstack/start` package, `app.config.ts` with Vinxi, `createAPIFileRoute` or `createServerFileRoute`.
- Do not edit `routeTree.gen.ts`, declare `Register` by hand in a Start app, or put the React plugin before `tanstackStart()`.
- Do not trust `beforeLoad` to protect data, put secrets in loaders, or use `forwardRef` in React 19 components.

## Before you finish

- Run `tsc --noEmit` (or the project's typecheck script) and the linter, with the route tree regenerated by the dev server or a build.
- Run a production build: import protection fails it when server code reaches the client.
- Test the first SSR load (view source), client navigation, a mutation followed by a refresh, and a direct anonymous call to every protected server function and server route.
