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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-start" with this command: npx skills add tenequm/claude-plugins/tenequm-claude-plugins-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

  1. 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

  1. 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() ], })

  1. 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> ), })

  1. 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 loader s 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.ts for server fn wrappers, .server.ts for internal helpers, .ts for 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 - true for SEO, false for 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 IDs

  • references/middleware.md

  • sendContext, custom fetch, global config, environment tree shaking

  • references/ssr-modes.md

  • Selective SSR inheritance, functional form, shellComponent, fallback rendering

  • references/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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

chrome-extension-wxt

No summary provided by upstream source.

Repository SourceNeeds Review
General

founder-playbook

No summary provided by upstream source.

Repository SourceNeeds Review
General

skill-finder

No summary provided by upstream source.

Repository SourceNeeds Review