Skip to main content

Overview

This quickstart guide will help you build a complete TanStack Start application with:
  • File-based routing
  • Server-side rendering (SSR)
  • Server functions for data fetching
  • Type-safe RPC calls
  • Streaming with Suspense
By the end, you’ll have a working full-stack application that fetches data from a server function and renders it with SSR.

Step 1: Create Your Project

Start by creating a new directory and initializing a project:
mkdir my-start-app
cd my-start-app
npm init -y

Step 2: Install Dependencies

Install TanStack Start and its dependencies:
npm install @tanstack/react-start @tanstack/react-router react react-dom
npm install -D @tanstack/react-router-devtools @types/react @types/react-dom typescript vite @vitejs/plugin-react nitro

Step 3: Configure Vite

Create a vite.config.ts file:
vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { nitro } from 'nitro/vite'

export default defineConfig({
  plugins: [
    tanstackStart(),
    viteReact(),
    nitro(),
  ],
})

Step 4: Configure TypeScript

Create a tsconfig.json file:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["vite/client"]
  },
  "include": ["src/**/*"]
}

Step 5: Create a Server Function

Create a server function to fetch data. This code runs only on the server:
src/utils/posts.ts
import { createServerFn } from '@tanstack/react-start'

export type Post = {
  id: number
  title: string
  body: string
}

// Server function to fetch posts
export const fetchPosts = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log('Fetching posts on server...')
    
    // This runs only on the server
    const res = await fetch('https://jsonplaceholder.typicode.com/posts')
    
    if (!res.ok) {
      throw new Error('Failed to fetch posts')
    }
    
    const posts = await res.json()
    return (posts as Array<Post>).slice(0, 10)
  }
)

// Server function to fetch a single post
export const fetchPost = createServerFn({ method: 'POST' })
  .inputValidator((id: string) => id)
  .handler(async ({ data: postId }) => {
    console.log(`Fetching post ${postId} on server...`)
    
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${postId}`
    )
    
    if (!res.ok) {
      throw new Error('Failed to fetch post')
    }
    
    return await res.json() as Post
  })
Server functions are automatically transformed into API endpoints by the Vite plugin. The client-side calls become type-safe RPC requests.

Step 6: Create the Root Route

Create the root route with SSR setup:
src/routes/__root.tsx
import {
  HeadContent,
  Link,
  Scripts,
  createRootRoute,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import * as React from 'react'

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: 'utf-8',
      },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1',
      },
    ],
  }),
  shellComponent: RootDocument,
})

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        <nav style={{ padding: '1rem', display: 'flex', gap: '1rem' }}>
          <Link to="/" activeProps={{ style: { fontWeight: 'bold' } }}>
            Home
          </Link>
          <Link to="/posts" activeProps={{ style: { fontWeight: 'bold' } }}>
            Posts
          </Link>
        </nav>
        <hr />
        <main style={{ padding: '1rem' }}>
          {children}
        </main>
        <TanStackRouterDevtools position="bottom-right" />
        <Scripts />
      </body>
    </html>
  )
}

Key Components for SSR

  • HeadContent: Renders meta tags, links, and scripts in the <head>
  • Scripts: Injects necessary client-side scripts for hydration
  • shellComponent: The outer HTML shell rendered on the server

Step 7: Create Route Pages

Create your route pages with data loading:
src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({ 
  component: Home,
})

function Home() {
  return (
    <div>
      <h1>Welcome to TanStack Start!</h1>
      <p>A full-stack framework built on TanStack Router.</p>
    </div>
  )
}
src/routes/posts.index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../utils/posts'

export const Route = createFileRoute('/posts/')({ 
  loader: () => fetchPosts(),
  component: PostsPage,
})

function PostsPage() {
  const posts = Route.useLoaderData()
  
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <strong>{post.title}</strong>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  )
}

Step 8: Create Router Configuration

Create the router configuration file:
src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  return createRouter({
    routeTree,
    defaultPreload: 'intent',
  })
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof getRouter>
  }
}

Step 9: Generate Route Tree

Generate the route tree file. The TanStack Start plugin will auto-generate this file, but you need to create an initial version:
src/routeTree.gen.ts
// This file is auto-generated by TanStack Router
import { Route as rootRoute } from './routes/__root'
import { Route as postsIndexRoute } from './routes/posts.index'
import { Route as indexRoute } from './routes/index'

export const routeTree = rootRoute.addChildren([
  indexRoute,
  postsIndexRoute,
])
In development, TanStack Router’s Vite plugin will automatically regenerate this file as you add or modify routes.

Step 10: Add Package Scripts

Update your package.json with the necessary scripts:
package.json
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "start": "node .output/server/index.mjs"
  }
}

Step 11: Run Your Application

Start the development server:
npm run dev
Your application will be available at http://localhost:5173 (or another port if 5173 is in use).

What’s Happening?

  1. SSR: Pages are rendered on the server first
  2. Server Functions: The fetchPosts function runs on the server
  3. Hydration: The client-side JavaScript makes the page interactive
  4. Type Safety: Full type safety from server to client

Step 12: Test Server Functions

Visit http://localhost:5173/posts and check your server logs. You should see:
Fetching posts on server...
This confirms that the server function is running on the server, not the client.

Advanced: Streaming with Suspense

Add streaming support for better perceived performance:
src/routes/posts.deferred.tsx
import { Await, createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { Suspense } from 'react'

const slowServerFn = createServerFn({ method: 'GET' }).handler(
  async () => {
    await new Promise((r) => setTimeout(r, 2000))
    return { message: 'This loaded slowly!' }
  }
)

export const Route = createFileRoute('/posts/deferred')({ 
  loader: async () => {
    return {
      // Don't await this - it will stream
      deferredData: slowServerFn(),
    }
  },
  component: DeferredPage,
})

function DeferredPage() {
  const { deferredData } = Route.useLoaderData()
  
  return (
    <div>
      <h1>Deferred Loading</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Await promise={deferredData}>
          {(data) => <p>{data.message}</p>}
        </Await>
      </Suspense>
    </div>
  )
}
The page will render immediately with the fallback, then stream in the deferred data when ready.

Build for Production

Build your application for production:
npm run build
This creates optimized builds in the .output directory:
  • .output/public/: Static client assets
  • .output/server/: Server bundle
Run the production server:
npm start

Next Steps

Congratulations! You’ve built your first TanStack Start application. Here’s what to explore next:

Server Functions

Deep dive into server functions, validation, and error handling

SSR & Streaming

Learn about advanced SSR and streaming patterns

API Routes

Create custom API endpoints for external clients

Deployment

Deploy your app to Netlify, Vercel, Cloudflare, and more

Build docs developers (and LLMs) love