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