This guide will help you learn the basics behind how TanStack Start works, regardless of how you set up your project.
TanStack Start is (currently*) powered by Vinxi, Nitro and TanStack Router.
Note
Vinxi will be removed before version 1.0.0 is released and TanStack will rely only on Vite and Nitro. The commands and APIs that use Vinxi will likely be replaced with a Vite plugin.
This is the file that will dictate the behavior of TanStack Router used within Start. Here, you can configure everything from the default preloading functionality to caching staleness.
// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createRouter() {
const router = createTanStackRouter({
routeTree,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}
// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createRouter() {
const router = createTanStackRouter({
routeTree,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}
The routeTree.gen.ts file is generated when you run TanStack Start (via npm run dev or npm run start) for the first time. This file contains the generated route tree and and handful of TS utilities that make TanStack Start fully type-safe.
Althoug TanStack Start is designed with client-first APIs, it is by and large, a full-stack framework. This means that all use cases, including both dynamic and static rely on a server or build-time entry to render our application's initial HTML payload.
This is done via the app/ssr.tsx file:
// app/ssr.tsx
/// <reference types="vinxi/types/server" />
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'
import { createRouter } from './router'
export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler)
// app/ssr.tsx
/// <reference types="vinxi/types/server" />
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'
import { createRouter } from './router'
export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler)
Whether we are statically generating our app or serving it dynamically, the ssr.tsx file is the entry point for doing all SSR-related work.
Getting our html to the client is only half the battle. Once there, we need to hydrate our client-side JavaScript once the route resolves to the client. We do this by hydrating the root of our application with the StartClient component:
// app/client.tsx
/// <reference types="vinxi/types/client" />
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'
const router = createRouter()
hydrateRoot(document, <StartClient router={router} />)
// app/client.tsx
/// <reference types="vinxi/types/client" />
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'
const router = createRouter()
hydrateRoot(document, <StartClient router={router} />)
This enables us to kick off client-side routing once the user's initial server request has fulfilled.
Other than the client entry point, the __root route of your application is the entry point for your application. The code in this file will wrap all other routes in the app, including your home page. It behaves like a layout route for your whole application.
Because it is always rendered, it is the perfect place to construct your application shell and take care of any global logic.
// app/routes/__root.tsx
import {
Outlet,
ScrollRestoration,
createRootRoute,
} from '@tanstack/react-router'
import { Meta, Scripts } from '@tanstack/start'
import type { ReactNode } from 'react'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
}),
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<Meta />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}
// app/routes/__root.tsx
import {
Outlet,
ScrollRestoration,
createRootRoute,
} from '@tanstack/react-router'
import { Meta, Scripts } from '@tanstack/start'
import type { ReactNode } from 'react'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
}),
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<Meta />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}
Routes are an extensive feature of TanStack Router, and are covered thoroughly in the Routing Guide. As a summary:
// app/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'
const filePath = 'count.txt'
async function readCount() {
return parseInt(
await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
)
}
const getCount = createServerFn({
method: 'GET',
}).handler(() => {
return readCount()
})
const updateCount = createServerFn({ method: 'POST' })
.validator((d: number) => d)
.handler(async ({ data }) => {
const count = await readCount()
await fs.promises.writeFile(filePath, `${count + data}`)
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => await getCount(),
})
function Home() {
const router = useRouter()
const state = Route.useLoaderData()
return (
<button
type="button"
onClick={() => {
updateCount({ data: 1 }).then(() => {
router.invalidate()
})
}}
>
Add 1 to {state}?
</button>
)
}
// app/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'
const filePath = 'count.txt'
async function readCount() {
return parseInt(
await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
)
}
const getCount = createServerFn({
method: 'GET',
}).handler(() => {
return readCount()
})
const updateCount = createServerFn({ method: 'POST' })
.validator((d: number) => d)
.handler(async ({ data }) => {
const count = await readCount()
await fs.promises.writeFile(filePath, `${count + data}`)
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => await getCount(),
})
function Home() {
const router = useRouter()
const state = Route.useLoaderData()
return (
<button
type="button"
onClick={() => {
updateCount({ data: 1 }).then(() => {
router.invalidate()
})
}}
>
Add 1 to {state}?
</button>
)
}
TanStack Start builds 100% on top of TanStack Router, so all of the navigation features of TanStack Router are available to you. In summary:
Here's a quick example of how you can use the Link component to navigate to a new route:
import { Link } from '@tanstack/react-router'
function Home() {
return <Link to="/about">About</Link>
}
import { Link } from '@tanstack/react-router'
function Home() {
return <Link to="/about">About</Link>
}
For more in-depth information on navigation, check out the navigation guide.
You may have noticed the server function we created above using createServerFn. This is one of TanStack's most powerful features, allowing you to create server-side functions that can be called from both the server during SSR and the client!
Here's a quick overview of how server functions work:
Here's a quick example of how you can use server functions to fetch and return data from the server:
import { createServerFn } from '@tanstack/start'
import * as fs from 'node:fs'
import { z } from 'zod'
const getUserById = createServerFn({ method: 'GET' })
// Always validate data sent to the function, here we use Zod
.validator(z.string())
// The handler function is where you perform the server-side logic
.handler(async ({ data }) => {
return db.query.users.findFirst({ where: eq(users.id, data) })
})
// Somewhere else in your application
const user = await getUserById({ data: '1' })
import { createServerFn } from '@tanstack/start'
import * as fs from 'node:fs'
import { z } from 'zod'
const getUserById = createServerFn({ method: 'GET' })
// Always validate data sent to the function, here we use Zod
.validator(z.string())
// The handler function is where you perform the server-side logic
.handler(async ({ data }) => {
return db.query.users.findFirst({ where: eq(users.id, data) })
})
// Somewhere else in your application
const user = await getUserById({ data: '1' })
To learn more about server functions, check out the server functions guide.
Server Functions can also be used to perform mutations on the server. This is also done using the same createServerFn function, but with the additional requirement that you invalidate any data on the client that was affected by the mutation.
Here's a quick example of how you can use server functions to perform a mutation on the server and invalidate the data on the client:
import { createServerFn } from '@tanstack/start'
const UserSchema = z.object({
id: z.string(),
name: z.string(),
})
const updateUser = createServerFn({ method: 'POST' })
.validator(UserSchema)
.handler(async ({ data }) => {
return db
.update(users)
.set({ name: data.name })
.where(eq(users.id, data.id))
})
// Somewhere else in your application
await updateUser({ data: { id: '1', name: 'John' } })
import { createServerFn } from '@tanstack/start'
const UserSchema = z.object({
id: z.string(),
name: z.string(),
})
const updateUser = createServerFn({ method: 'POST' })
.validator(UserSchema)
.handler(async ({ data }) => {
return db
.update(users)
.set({ name: data.name })
.where(eq(users.id, data.id))
})
// Somewhere else in your application
await updateUser({ data: { id: '1', name: 'John' } })
To learn more about mutations, check out the mutations guide.
Another powerful feature of TanStack Router is data loading. This allows you to fetch data for SSR and preload route data before it is rendered. This is done using the loader function of a route.
Here's a quick overview of how data loading works:
To learn more about data loading, check out the data loading guide.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.