From tanstack
Builds full-stack React apps with TanStack Start for SSR/SSG/streaming, server functions, API routes, middleware, and deployments to Cloudflare Workers/Vercel/Node/Bun/Docker.
npx claudepluginhub tenequm/claude-pluginsThis skill uses the workspace's default tool permissions.
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.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
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).
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.
pnpm create @tanstack/start@latest
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
// 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()
],
})
// 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>
),
})
// 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>
},
})
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
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 |
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
})
// 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>
}
Access request/response from @tanstack/react-start/server: getRequest(), getRequestHeader(name), setResponseHeaders(headers), setResponseStatus(code).
Two types: request middleware (all server requests including SSR) and server function middleware (server functions only, with client-side hooks and input validation).
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
})
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
})
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 } })
})
import { createStart, createMiddleware } from '@tanstack/react-start'
export const startInstance = createStart(() => ({
requestMiddleware: [globalLogger], // ALL requests (SSR, routes, fns)
functionMiddleware: [globalAuth], // ALL server functions
}))
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.
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,
})
Ship static HTML shells with server function support but no SSR:
// vite.config.ts
tanstackStart({ spa: { enabled: true } })
// src/start.ts
export const startInstance = createStart(() => ({ defaultSsr: false }))
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,
})
// 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 },
})
}
// 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>
},
})
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>
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.
Install @netlify/vite-plugin-tanstack-start, add netlify() to vite plugins alongside tanstackStart().
Install nitro@npm:nitro-nightly@latest, add nitro() to vite plugins. Build with vite build, run with node .output/server/index.mjs.
tanstackStart({ prerender: { enabled: true, crawlLinks: true } })
createServerFn() for server-only access..functions.ts for server fn wrappers, .server.ts for internal helpers, .ts for shared types/schemas.head() on every content route - Title, description, OG tags. Use loader data for dynamic pages.true for SEO, false for browser-only, 'data-only' for dashboards..inputValidator().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