--- name: tanstack-start description: "Best practices for TanStack Start, the full-stack React framework built on TanStack Router and Vite/Nitro. Use when building server functions with createServerFn, setting up SSR and streaming with defer()/Await, writing API routes, configuring app.config.ts, or deploying a TanStack Start app to Node, Vercel, Netlify, Bun, or Cloudflare." --- # TanStack Start TanStack Start is a full-stack React framework that layers server functions, SSR, streaming, and API routes on top of TanStack Router and a Vite/Nitro build pipeline, giving end-to-end type safety from server to client. ## Workflow for Building a TanStack Start Feature 1. **Configure the app** — Set up `app.config.ts` with the Vite plugins and a `server.preset` matching the deployment target. 2. **Define the root route** — Create `src/routes/__root.tsx` with the HTML document shell (``, ``, ``, ``, ``, ``). 3. **Write server functions** — Use `createServerFn()` with a `.validator()` and `.handler()` for any logic that must run on the server (database access, secrets, file I/O). 4. **Wire server functions into routes** — Call server functions from route `loader`s for reads and from mutation hooks (`useMutation`) for writes. 5. **Add API routes for raw HTTP** — Use `createAPIFileRoute` for webhooks, OAuth callbacks, or endpoints that external services call directly. 6. **Stream non-critical data** — Wrap slow, non-blocking loader data in `defer()` and render it with `` + ``. 7. **Deploy** — Pick the `server.preset` for the target platform and verify environment variable access rules before shipping. ## Core Principles - TanStack Start = TanStack Router + Vite + Nitro, giving full-stack React with a single type-safe codebase. - `createServerFn` is the primary mechanism for server-side logic — prefer it over hand-rolled REST endpoints for internal data access. - All TanStack Router conventions apply on top of Start: file-based routing, loaders, search param validation, and router context. - Streaming and Suspense are first-class citizens; use `defer()` for anything that shouldn't block the initial response. - Keep server-only code (secrets, DB clients, filesystem access) strictly inside server functions or API routes — never import it into client components. ## Configuration ### app.config.ts ```ts import { defineConfig } from '@tanstack/start/config' import tsConfigPaths from 'vite-tsconfig-paths' export default defineConfig({ vite: { plugins: [tsConfigPaths()] }, server: { preset: 'node-server', // or: 'vercel', 'netlify', 'bun', 'cloudflare-pages' }, }) ``` ### Root Route HTML Shell ```tsx // src/routes/__root.tsx import { createRootRoute, Outlet, ScrollRestoration } from '@tanstack/react-router' import { Scripts } from '@tanstack/start' export const Route = createRootRoute({ component: () => ( ), }) ``` ## Server Functions - Define server functions in a dedicated `src/server/functions/` (or colocated `*.server.ts`) module for discoverability. - Always validate input with `.validator()` (Zod is the common choice) before the handler runs — never trust client-supplied `data`. - Throw descriptive `Error`s (or a typed error) from handlers; TanStack Start serializes thrown errors back to the client. - Server functions can be called directly from loaders, actions, event handlers, or `useMutation` — they behave like normal async functions on the client but execute on the server. ```ts // src/server/functions/posts.ts import { createServerFn } from '@tanstack/start' import { z } from 'zod' import { db } from '~/server/db' export const getPost = createServerFn() .validator(z.object({ id: z.string() })) .handler(async ({ data }) => { const post = await db.post.findUnique({ where: { id: data.id } }) if (!post) throw new Error('Post not found') return post }) export const createPost = createServerFn() .validator(z.object({ title: z.string().min(1), body: z.string() })) .handler(async ({ data }) => db.post.create({ data })) ``` ### Using Server Functions in Routes ```tsx export const Route = createFileRoute('/posts/$postId')({ loader: ({ params }) => getPost({ data: { id: params.postId } }), component: PostDetail, }) function PostDetail() { const post = Route.useLoaderData() return

{post.title}

} ``` ### Mutations with Server Functions ```tsx import { useMutation } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query' import { createPost } from '~/server/functions/posts' function NewPostForm() { const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: (input: { title: string; body: string }) => createPost({ data: input }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }), }) return (
{ e.preventDefault() const form = new FormData(e.currentTarget) mutation.mutate({ title: String(form.get('title')), body: String(form.get('body')), }) }} >