TanStack Start Tutorial for Complete Beginners
TanStack Start is a full-stack React framework built on top of TanStack Router. Think of
it like [Link], but powered by TanStack’s router/query ecosystem — you get file-based
routing, server-side rendering, and the ability to write server-only functions right next to
your components, without running a separate backend.
0. What You Need to Know First
Concept What it means
TanStack
The routing engine (file-based, fully type-safe)
Router
TanStack A framework wrapped around the Router that adds SSR, server functions,
Start and API routes
Vite The build tool Start uses under the hood
If you know React already, TanStack Start feels like: React + file-based routing + “server
functions” (like a mini backend) all in one project.
1. Install & Setup
Requirement
[Link] installed (check with node -v )
Fastest way — scaffold with the CLI
npx @tanstack/cli@latest create my-app
This walks you through prompts:
Project name
Package manager (npm/pnpm/yarn/bun)
Add-ons (TanStack Query, Tailwind, Clerk auth, Drizzle ORM, etc. — skip these for now
as a beginner)
Then:
cd my-app
npm install
npm run dev
Visit [Link]
You can also do --add-ons tailwind etc. directly as flags if you already know what you
want.
Manual setup (good for understanding what’s happening)
mkdir my-app && cd my-app
npm init -y
npm i @tanstack/react-start @tanstack/react-router react react-dom
npm i -D vite @vitejs/plugin-react typescript @types/react @types/react-dom
Then create [Link] , [Link] , and a src/routes folder — but honestly,
use the CLI for your first project. Manual setup is for later once you understand the pieces.
2. Project Structure
my-app/
├── src/
│ ├── routes/ ← THIS is where routing magic happens
│ │ ├── __root.tsx ← the root layout (always renders)
│ │ ├── [Link] ← "/"
│ │ ├── [Link] ← "/about"
│ │ └── posts/
│ │ ├── [Link] ← "/posts"
│ │ └── $[Link] ← "/posts/:postId" (dynamic)
│ ├── [Link] ← router configuration
│ └── [Link] ← AUTO-GENERATED, don't edit this
├── [Link]
└── [Link]
Key idea: the file/folder structure under src/routes = your URL structure. This is exactly
like [Link]’s app/ folder, but the config lives in [Link] and route generation happens
through a Vite plugin.
3. How Routing Works (Step by Step)
The Root Route — __root.tsx
Every app needs this. It wraps all pages — like a global layout ( <html> , <body> , nav bar,
etc.)
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<html>
<head></head>
<body>
<nav>
<a href="/">Home</a> | <a href="/about">About</a>
</nav>
<Outlet /> {/* child route renders here */}
</body>
</html>
),
})
<Outlet /> is where the matched child page gets injected — same concept as {children}
in [Link] layouts.
A Basic Page — [Link] (matches / )
// src/routes/[Link]
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: HomePage,
})
function HomePage() {
return <h1>Welcome Home!</h1>
}
Another Page — [Link] (matches /about )
// src/routes/[Link]
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: () => <h1>About Us</h1>,
})
Dynamic Routes — $[Link] (matches /posts/123 )
The $ prefix means “this is a URL parameter” (equivalent to [id] in [Link], or :id in
Express).
// src/routes/posts/$[Link]
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
component: PostPage,
})
function PostPage() {
const { postId } = [Link]()
return <h1>Viewing Post #{postId}</h1>
}
Nested / Layout Routes
A file named the same as a folder (e.g. [Link] next to a posts/ folder) becomes a
layout for everything inside that folder:
routes/
├── [Link] ← layout wrapper for all /posts/* routes
└── posts/
├── [Link] ← /posts
└── $[Link] ← /posts/:postId
// [Link]
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
component: () => (
<div>
<h2>Posts Section</h2>
<Outlet /> {/* [Link] or $[Link] renders here */}
</div>
),
})
Pathless / Group Layouts
A folder or file prefixed with _ doesn’t add a URL segment — it’s just for grouping/sharing
layout without affecting the path (similar to [Link]’s (group) folders).
404 / Not Found Route
// src/routes/__root.tsx
export const Route = createRootRoute({
component: RootComponent,
notFoundComponent: () => <p>Page not found!</p>,
})
4. Reading Query Params & Search Params
// src/routes/[Link]
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/search')({
component: SearchPage,
validateSearch: (search: Record<string, unknown>) => ({
q: (search.q as string) || '',
page: Number([Link]) || 1,
}),
})
function SearchPage() {
const { q, page } = [Link]()
return <p>Searching "{q}", page {page}</p>
}
Visiting /search?q=laptop&page=2 → q="laptop", page=2 . TanStack Router validates and
type-checks your search params — a big upgrade over plain URLSearchParams .
Linking Between Pages (type-safe navigation)
import { Link } from '@tanstack/react-router'
<Link to="/posts/$postId" params={{ postId: '5' }}>
Go to Post 5
</Link>
<Link to="/search" search={{ q: 'shoes', page: 1 }}>
Search Shoes
</Link>
If you typo a route name or forget a required param, TypeScript will catch it at compile
time.
5. Loaders — Fetching Data Before the Page Renders
A loader runs before your component mounts (great for SSR — no loading spinners on first
paint).
// src/routes/posts/$[Link]
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const res = await fetch(`[Link]
return [Link]()
},
component: PostPage,
})
function PostPage() {
const post = [Link]()
return <h1>{[Link]}</h1>
}
6. Server Functions — Your “Backend” Without a Separate
Server
This is TanStack Start’s headline feature: write a function that only ever runs on the server,
and call it directly from your React component like a normal function.
// src/routes/[Link]
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const getServerTime = createServerFn({ method: 'GET' }).handler(async () => {
return new Date().toISOString() // this code NEVER reaches the browser bundle
})
export const Route = createFileRoute('/')({
loader: () => getServerTime(),
component: HomePage,
})
function HomePage() {
const time = [Link]()
return <p>Server time: {time}</p>
}
Compare to Node/Express thinking: this replaces writing a separate /api/time route +
fetch() call — it’s like an RPC call baked into the framework.
Server Function with Input (e.g. form submission)
import { createServerFn } from '@tanstack/react-start'
const createPost = createServerFn({ method: 'POST' })
.validator((data: { title: string }) => data)
.handler(async ({ data }) => {
// save to database here
return { id: [Link](), title: [Link] }
})
7. Server Routes — True API Endpoints (for external clients,
webhooks, etc.)
If you need a classic REST-style API endpoint (not just called from your own React app), add
a server property to a route:
// src/routes/api/[Link]
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: () => json({ message: 'Hello from API route!' }),
POST: async ({ request }) => {
const body = await [Link]()
return json({ received: body })
},
},
},
})
This behaves exactly like an Express route — visiting /api/hello in the browser or via
fetch /Postman hits this handler directly.
8. Full CRUD Example (server functions + a page)
// src/routes/[Link]
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { useState } from 'react'
// --- fake in-memory "database" ---
let todos: { id: number; text: string }[] = []
// CREATE
const addTodo = createServerFn({ method: 'POST' })
.validator((text: string) => text)
.handler(async ({ data }) => {
const todo = { id: [Link](), text: data }
[Link](todo)
return todo
})
// READ
const getTodos = createServerFn({ method: 'GET' }).handler(async () => todos)
// UPDATE
const updateTodo = createServerFn({ method: 'POST' })
.validator((data: { id: number; text: string }) => data)
.handler(async ({ data }) => {
todos = [Link]((t) => ([Link] === [Link] ? { ...t, text: [Link] } : t))
return todos
})
// DELETE
const deleteTodo = createServerFn({ method: 'POST' })
.validator((id: number) => id)
.handler(async ({ data }) => {
todos = [Link]((t) => [Link] !== data)
return todos
})
export const Route = createFileRoute('/todos')({
loader: () => getTodos(),
component: TodosPage,
})
function TodosPage() {
const initialTodos = [Link]()
const [todos, setTodos] = useState(initialTodos)
const [text, setText] = useState('')
return (
<div>
<input value={text} onChange={(e) => setText([Link])} />
<button
onClick={async () => {
const newTodo = await addTodo({ data: text })
setTodos([...todos, newTodo])
setText('')
}}
>
Add
</button>
<ul>
{[Link]((t) => (
<li key={[Link]}>
{[Link]}
<button onClick={async () => setTodos(await deleteTodo({ data: [Link] }))}>
Delete
</button>
</li>
))}
</ul>
</div>
)
}
9. Quick Reference — Routing Cheat Sheet
File URL Matched
routes/[Link] /
routes/[Link] /about
routes/posts/[Link] /posts
routes/posts/$[Link] /posts/:postId (dynamic)
routes/[Link] + posts/ folder Layout wrapping all /posts/*
routes/_layout.tsx Pathless layout (no URL segment)
routes/files/$.tsx Splat/catch-all route
routes/api/[Link] (with server ) Real API endpoint /api/hello
Concept Node/Express-style thinking TanStack Start equivalent
[Link]('/posts/:id',
Routing posts/$[Link] file
...)
API endpoint Express route handler [Link] in a route file
Fetching before
Manual useEffect + fetch loader
render
createServerFn() called
Calling backend logic fetch('/api/...")
directly
10. Run, Build & Deploy
npm run dev # local dev server
npm run build # production build
npm run start # run the production build
Start can deploy to Netlify, Vercel, Node servers, Cloudflare Workers, and more — each with
a small adapter/plugin (e.g. @netlify/vite-plugin-tanstack-start ).
That’s the full loop: install → file-based routing → dynamic params/search params → loaders
→ server functions → API routes → CRUD → deploy. Want me to extend this with TanStack
Query integration for caching/mutations, or authentication next?