tanstack-start
TanStack Start
Full-stack React framework powered by TanStack Router and Vite. Adds SSR, streaming, server functions, middleware, server routes, and universal deployment to TanStack Router's type-safe routing.
TanStack Start is in Release Candidate stage. API is stable and feature-complete. No RSC support yet (in active development).
When to Use This Skill
- Building full-stack React with SSR, SSG, or streaming
- Adding server functions (type-safe RPCs) to a React app
- Creating API/server routes alongside frontend routes
- Implementing middleware for auth, logging, or request handling
- Deploying to Cloudflare Workers, Netlify, Vercel, Node.js, Bun, or Docker
- Need SPA mode with optional server functions (no SSR required)
Use TanStack Router alone (see tanstack-router skill) when you only need client-side routing without server features.
For routing concepts (file-based routing, search params, nested layouts, loaders, preloading), refer to the tanstack-router skill. This skill covers Start-specific full-stack features.
Quick Start Workflow
1. Create Project
pnpm create @tanstack/start@latest
2. Manual Setup
npm i @tanstack/react-start @tanstack/react-router react react-dom
npm i -D vite @vitejs/plugin-react typescript @types/react @types/react-dom vite-tsconfig-paths
3. Vite Configuration
// vite.config.ts
import { defineConfig } from 'vite'
import tsConfigPaths from 'vite-tsconfig-paths'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
server: { port: 3000 },
plugins: [
tsConfigPaths(),
tanstackStart(),
viteReact(), // MUST come after tanstackStart()
],
})
4. Router and Root Route
// 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
/// <reference types="vite/client" />
import type { ReactNode } from 'react'
import { Outlet, createRootRoute, HeadContent, Scripts } from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'My TanStack Start App' },
],
}),
component: () => (
<html>
<head><HeadContent /></head>
<body><Outlet /><Scripts /></body>
</html>
),
})
5. Route with Server Function
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const getServerTime = createServerFn({ method: 'GET' }).handler(async () => {
return new Date().toISOString()
})
export const Route = createFileRoute('/')({
loader: () => getServerTime(),
component: () => {
const time = Route.useLoaderData()
return <div>Server time: {time}</div>
},
})
File Structure
src/
├── routes/
│ ├── __root.tsx # HTML shell, always rendered
│ └── index.tsx
├── router.tsx # Router config
├── routeTree.gen.ts # Auto-generated
├── start.ts # Optional: global middleware
└── server.ts # Optional: custom server entry
Execution Model
All code is isomorphic by default - runs in both server and client bundles unless constrained. Route loaders run on the server during SSR AND on the client during navigation.
// WRONG - secret exposed to client bundle
export const Route = createFileRoute('/users')({
loader: () => {
const secret = process.env.SECRET
return fetch(`/api/users?key=${secret}`)
},
})
// CORRECT - server function keeps secrets server-side
const getUsers = createServerFn().handler(async () => {
return fetch(`/api/users?key=${process.env.SECRET}`)
})
export const Route = createFileRoute('/users')({
loader: () => getUsers(),
})
| API | Runs On | Client Behavior |
|---|---|---|
createServerFn() |
Server | Network request (RPC) |
createServerOnlyFn(fn) |
Server | Throws error |
createClientOnlyFn(fn) |
Client | Works normally |
createIsomorphicFn() |
Both | Environment-specific impl |
<ClientOnly> |
Client | Renders fallback on server |
Server Functions
Type-safe RPCs via createServerFn(). Server code is extracted from client bundles at build time; client calls become fetch requests.
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { redirect, notFound } from '@tanstack/react-router'
// GET with no input
export const getData = createServerFn({ method: 'GET' }).handler(async () => {
return { message: 'Hello from server!' }
})
// POST with Zod validation
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(1),
})
export const createPost = createServerFn({ method: 'POST' })
.inputValidator(CreatePostSchema)
.handler(async ({ data }) => {
return await db.posts.create(data)
})
// Redirect and notFound
export const getPost = createServerFn()
.inputValidator((data: { id: string }) => data)
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) throw notFound()
return post
})
Calling Server Functions
// From loader
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})
// From component with useServerFn
import { useServerFn } from '@tanstack/react-start'
function CreatePostForm() {
const mutation = useServerFn(createPost)
return <button onClick={() => mutation({ data: { title: 'New', body: 'Content' } })}>Create</button>
}
// Direct call with router.invalidate()
function DeleteButton({ id }: { id: string }) {
const router = useRouter()
return <button onClick={() => deletePost({ data: { id } }).then(() => router.invalidate())}>Delete</button>
}
Server Context Utilities
Access request/response from @tanstack/react-start/server: getRequest(), getRequestHeader(name), setResponseHeaders(headers), setResponseStatus(code).
Middleware
Two types: request middleware (all server requests including SSR) and server function middleware (server functions only, with client-side hooks and input validation).
Request Middleware
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware().server(async ({ next, request }) => {
const start = Date.now()
const result = await next()
console.log(`${request.method} ${request.url} - ${Date.now() - start}ms`)
return result
})
Server Function Middleware with Context
const authMiddleware = createMiddleware({ type: 'function' })
.server(async ({ next }) => {
const user = await getCurrentUser()
if (!user) throw redirect({ to: '/login' })
return next({ context: { user } })
})
const getProfile = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
return context.user // typed
})
Client + Server Middleware
const authHeaderMiddleware = createMiddleware({ type: 'function' })
.client(async ({ next }) => {
return next({ headers: { Authorization: `Bearer ${getToken()}` } })
})
.server(async ({ next }) => {
const user = await verifyToken(getRequestHeader('Authorization'))
return next({ context: { user } })
})
Global Middleware (src/start.ts)
import { createStart, createMiddleware } from '@tanstack/react-start'
export const startInstance = createStart(() => ({
requestMiddleware: [globalLogger], // ALL requests (SSR, routes, fns)
functionMiddleware: [globalAuth], // ALL server functions
}))
Server Routes
HTTP endpoints alongside frontend routes using file-based routing. Handlers receive { request, params, context } and return Response.
// src/routes/api/users.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/users')({
server: {
middleware: [authMiddleware],
handlers: {
GET: async ({ request }) => {
return Response.json(await db.users.findMany())
},
POST: async ({ request }) => {
const body = await request.json()
return Response.json(await db.users.create(body), { status: 201 })
},
},
},
})
Per-handler middleware via createHandlers:
server: {
handlers: ({ createHandlers }) => createHandlers({
GET: async ({ request }) => Response.json({ ok: true }),
DELETE: {
middleware: [adminOnlyMiddleware],
handler: async ({ request }) => Response.json({ deleted: true }),
},
}),
}
Server routes and components can co-exist in the same file. Dynamic params ($id), wildcards ($), and escaped matching ([.]json) all work identically to Router.
SSR Modes
Per-route SSR control via the ssr property:
| Mode | Loaders | Component | Use Case |
|---|---|---|---|
true (default) |
Server + Client | Server + Client | SEO, performance |
false |
Client only | Client only | Browser APIs, canvas |
'data-only' |
Server + Client | Client only | Dashboards |
(params, search) => ... |
Dynamic | Dynamic | Conditional SSR |
export const Route = createFileRoute('/dashboard')({
ssr: 'data-only',
loader: () => getDashboardData(),
component: Dashboard,
})
SPA Mode
Ship static HTML shells with server function support but no SSR:
// vite.config.ts
tanstackStart({ spa: { enabled: true } })
Global Default
// src/start.ts
export const startInstance = createStart(() => ({ defaultSsr: false }))
Head Management and SEO
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => ({ post: await getPost({ data: { id: params.postId } }) }),
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.post.title },
{ name: 'description', content: loaderData.post.excerpt },
{ property: 'og:title', content: loaderData.post.title },
{ property: 'og:image', content: loaderData.post.coverImage },
{ name: 'twitter:card', content: 'summary_large_image' },
],
links: [{ rel: 'canonical', href: `https://myapp.com/posts/${loaderData.post.id}` }],
}),
component: PostPage,
})
Authentication
Session Management
// utils/session.ts
import { useSession } from '@tanstack/react-start/server'
export function useAppSession() {
return useSession<{ userId?: string; email?: string }>({
name: 'app-session',
password: process.env.SESSION_SECRET!,
cookie: { secure: process.env.NODE_ENV === 'production', sameSite: 'lax', httpOnly: true },
})
}
Route Protection
// src/routes/_authed.tsx - layout route guard
export const Route = createFileRoute('/_authed')({
beforeLoad: async ({ location }) => {
const user = await getCurrentUserFn()
if (!user) throw redirect({ to: '/login', search: { redirect: location.href } })
return { user }
},
})
// src/routes/_authed/dashboard.tsx - automatically protected
export const Route = createFileRoute('/_authed/dashboard')({
component: () => {
const { user } = Route.useRouteContext()
return <h1>Welcome, {user.email}!</h1>
},
})
Environment Functions
import { createIsomorphicFn, createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'
import { ClientOnly } from '@tanstack/react-router'
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))
const getDbUrl = createServerOnlyFn(() => process.env.DATABASE_URL) // throws on client
const saveLocal = createClientOnlyFn((k: string, v: string) => localStorage.setItem(k, v)) // throws on server
// Component-level: renders fallback during SSR, children after hydration
<ClientOnly fallback={<div>Loading...</div>}><InteractiveChart /></ClientOnly>
Deployment
Cloudflare Workers (Official Partner)
Install @cloudflare/vite-plugin and wrangler, add cloudflare({ viteEnvironment: { name: 'ssr' } }) to vite plugins (before tanstackStart()), and set "main": "@tanstack/react-start/server-entry" in wrangler.jsonc.
Netlify (Official Partner)
Install @netlify/vite-plugin-tanstack-start, add netlify() to vite plugins alongside tanstackStart().
Nitro (Node.js, Vercel, Bun, Docker)
Install nitro@npm:nitro-nightly@latest, add nitro() to vite plugins. Build with vite build, run with node .output/server/index.mjs.
Static Prerendering
tanstackStart({ prerender: { enabled: true, crawlLinks: true } })
Best Practices
- Never put secrets in loaders - Loaders are isomorphic. Use
createServerFn()for server-only access. - Server functions are the boundary - Primary mechanism for safe server-only execution from client code.
- Organize by concern -
.functions.tsfor server fn wrappers,.server.tsfor internal helpers,.tsfor shared types/schemas. - Compose middleware hierarchically - Global for cross-cutting concerns, route-level for groups, function-level for specifics.
- Use
head()on every content route - Title, description, OG tags. Use loader data for dynamic pages. - Choose SSR mode per route -
truefor SEO,falsefor browser-only,'data-only'for dashboards. - Validate all server function inputs - Zod or custom validators via
.inputValidator().
Advanced Topics
For deeper coverage, see reference files:
references/server-functions.md- Streaming, FormData, progressive enhancement, request cancellation, custom function IDsreferences/middleware.md- sendContext, custom fetch, global config, environment tree shakingreferences/ssr-modes.md- Selective SSR inheritance, functional form, shellComponent, fallback renderingreferences/server-routes.md- Dynamic params, wildcards, escaped matching, pathless layouts
Resources
- Official Docs
- GitHub (Start lives in the router repo)
- Examples - Basic, Auth, React Query, Cloudflare, Clerk, Supabase
- Start vs Next.js
- Cross-reference: tanstack-router skill for routing, tanstack-query skill for data fetching
More from tenequm/claude-plugins
chrome-extension-wxt
Build Chrome extensions using WXT framework with TypeScript, React, Vue, or Svelte. Use when creating browser extensions, developing cross-browser add-ons, or working with Chrome Web Store projects. Triggers on phrases like "chrome extension", "browser extension", "WXT framework", "manifest v3", or file patterns like wxt.config.ts.
96shadcn-tailwind
Build UIs with Tailwind CSS v4 and shadcn/ui. Covers CSS variables with OKLCH colors, component variants with CVA, responsive design, dark mode, and Tailwind v4.2 features. Supports Radix UI and Base UI primitives, CLI 3.0, and visual styles. Use when building interfaces with Tailwind, styling shadcn/ui components, implementing themes, or working with utility-first CSS. Triggers on tailwind, shadcn, utility classes, CSS variables, OKLCH, component styling, theming, dark mode, radix ui.
75gh-cli
GitHub CLI for remote repository analysis, file fetching, codebase comparison, and discovering trending code/repos. Use when analyzing repos without cloning, comparing codebases, or searching for popular GitHub projects.
43impactful-writing
Write clear, emotionally resonant, and well-structured content that readers remember and act upon. Use when writing or editing any text—Twitter posts, articles, documentation, emails, comments, updates—for maximum clarity, engagement, and impact.
36skill-factory
Autonomous skill creation agent that analyzes requests, automatically selects the best creation method (documentation scraping via Skill_Seekers, manual TDD construction, or hybrid), ensures quality compliance with Anthropic best practices, and delivers production-ready skills without requiring user decision-making or navigation
33solana-development
Build Solana programs with Anchor framework or native Rust. Use when developing Solana smart contracts, implementing token operations, testing programs, deploying to networks, or working with Solana development. Covers both high-level Anchor framework (recommended) and low-level native Rust for advanced use cases.
30