TanStack Start v1 RC: The Full-Stack React Framework Built for Modern Development
Executive Summary
TanStack Start v1 Release Candidate marks a significant milestone in the evolution of full-stack React development. Built by the team behind the wildly successful TanStack Router, Query, Table, and Virtual libraries—which collectively power millions of production applications—TanStack Start emerges as a comprehensive full-stack framework that refuses to compromise on developer experience, type safety, or performance.
Unlike framework giants backed by venture capital and corporate interests, TanStack Start is 100% open source, community-driven, and bootstrapped through sustainable partnerships with technology companies. This independence enables a unique design philosophy: client-first development that doesn't sacrifice server-side capabilities, complete type safety from route definitions to server functions, and a unified route tree that eliminates the disconnection between client and server logic.
The Release Candidate status signals that TanStack Start is feature-complete with a stable API, ready for adventurous teams to deploy in production while the final bugs are ironed out through community feedback. The framework delivers everything developers expect from modern full-stack solutions—full-document SSR, streaming responses, type-safe server functions, middleware composition, CSP/nonce support, and Zero-JS rendering capabilities—while maintaining the exceptional developer experience that made TanStack Router the fastest-growing routing library in React.
For development teams tired of framework lock-in, opaque abstractions, or choosing between client-side flexibility and server-side power, TanStack Start offers a refreshing alternative built on proven technologies (Vite, Vinxi, Nitro) with transparent implementation and complete control.
The Evolution of Full-Stack React
From Client-Side Routing to Full-Stack Framework
The TanStack ecosystem began with TanStack Router, which revolutionized client-side routing in React by introducing type-safe routing with automatic TypeScript inference, code-splitting out of the box, and innovative features like search parameter validation and route-level data loading. Developers loved the predictability and compile-time safety Router provided, eliminating entire classes of routing bugs.
As Router matured and gained adoption (reaching millions of weekly NPM downloads), the community repeatedly requested server-side capabilities. Not to replace client-side routing, but to augment it with server rendering, API routes, and progressive enhancement. The TanStack team recognized that bolting SSR onto an existing client-only router would compromise both approaches, so they embarked on building TanStack Start from the ground up.
The Full-Stack Challenge
Building a successful full-stack framework requires balancing competing priorities:
Type Safety vs. Runtime Flexibility: Developers want compile-time guarantees that their server functions match client expectations, but they also need the flexibility to handle dynamic runtime scenarios.
Client-First DX vs. Server Optimization: A great client-side development experience emphasizes fast iteration, hot module replacement, and local state management, while server optimization demands different bundling strategies, edge deployment considerations, and streaming responses.
Framework Abstractions vs. Transparency: Abstractions simplify common tasks but can create "magic" that becomes impossible to debug or customize when edge cases arise.
Universal Deployment vs. Platform Optimization: Applications should deploy anywhere (Vercel, Netlify, AWS, on-premise) while still taking advantage of platform-specific features when available.
TanStack Start addresses these challenges through architectural decisions that prioritize developer experience without sacrificing capability.
Key Features and Capabilities
Unified Route Tree Architecture
TanStack Start's most distinctive feature is its unified route tree that seamlessly integrates client and server logic within a single, type-safe structure. Unlike frameworks where client routes, server routes, and API endpoints live in separate directories with different conventions, Start uses a file-based routing system where each route file can contain:
Client-Side Route Components:
// routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
component: DashboardComponent,
errorComponent: DashboardError,
pendingComponent: DashboardSkeleton,
// Client-side validation and search params
validateSearch: (search) => ({
tab: search.tab as 'overview' | 'analytics' | 'settings'
})
})
function DashboardComponent() {
const { tab } = Route.useSearch()
return
Dashboard - {tab}
}
Server-Side Data Loading:
// Same file - routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'
// Type-safe server function
const getAnalytics = createServerFn('GET', async () => {
// This code ONLY runs on the server
const db = await connectToDatabase()
const data = await db.query('SELECT * FROM analytics')
return { analytics: data }
})
export const Route = createFileRoute('/dashboard')({
component: DashboardComponent,
loader: async () => {
// Type-safe call to server function
const data = await getAnalytics()
return data
}
})
function DashboardComponent() {
const data = Route.useLoaderData() // Fully typed!
return
{data.analytics.length} analytics records
}
This unified approach delivers several powerful benefits:
Complete Type Inference: TypeScript flows seamlessly from server function definitions through loaders to component props, catching type mismatches at compile time rather than runtime.
Colocation of Related Logic: The server function that loads data lives right next to the component that renders it, eliminating the mental overhead of jumping between different files and conventions.
Automatic Code Splitting: Start automatically extracts server-only code during build, ensuring database connections and API keys never ship to the client.
Progressive Enhancement: The same route definition works with JavaScript disabled, SSR-first, or fully client-side, depending on your configuration.
Type-Safe Server Functions
TanStack Start's server functions (createServerFn) provide a development experience that feels like calling local functions while executing securely on the server:
Basic Server Function:
import { createServerFn } from '@tanstack/start'
// Define a type-safe server function
export const getUserProfile = createServerFn('GET', async (userId: string) => {
// Server-only code - never sent to client
const db = await getDatabase()
const user = await db.users.findOne({ id: userId })
if (!user) {
throw new Error('User not found')
}
return {
id: user.id,
name: user.name,
email: user.email,
// Sensitive fields excluded automatically
}
})
// Call from component with full type safety
function ProfilePage() {
const [profile, setProfile] = useState(null)
useEffect(() => {
getUserProfile('user-123').then(setProfile)
}, [])
return
{profile?.name} // TypeScript knows the shape!
}
Server Functions with Middleware: Server functions compose beautifully with middleware for cross-cutting concerns:
import { createServerFn } from '@tanstack/start'
import { authMiddleware } from './middleware/auth'
// Protected server function requiring authentication
export const deletePost = createServerFn('POST', async (postId: string, ctx) => {
// Middleware ensures ctx.user exists
const user = ctx.user
const db = await getDatabase()
const post = await db.posts.findOne({ id: postId })
if (post.authorId !== user.id) {
throw new Error('Unauthorized')
}
await db.posts.delete({ id: postId })
return { success: true }
}).middleware([authMiddleware])
Mutation Server Functions: For operations that modify server state:
export const updateUserProfile = createServerFn('POST', async (data: {
name: string
bio: string
}) => {
const db = await getDatabase()
await db.users.update({
where: { id: data.userId },
data: {
name: data.name,
bio: data.bio,
updatedAt: new Date()
}
})
// Return updated data
return { success: true, updated: data }
})
// Use in component
function ProfileEditor() {
const mutation = useMutation({
mutationFn: updateUserProfile
})
const handleSubmit = (formData) => {
mutation.mutate(formData)
}
return (
)
}
Server functions automatically handle:
- •Serialization of function arguments and return values
- •Error propagation with proper error boundaries
- •Request deduplication for simultaneous calls
- •Automatic code elimination (server code never reaches client bundle)
Full-Document SSR with Streaming
TanStack Start implements modern SSR with streaming support, enabling faster time-to-first-byte and progressive rendering:
Automatic SSR: By default, routes matching the initial request render on the server:
// routes/blog/[slug].tsx
export const Route = createFileRoute('/blog/$slug')({
component: BlogPost,
loader: async ({ params }) => {
// Executed on server for initial SSR request
const post = await fetchBlogPost(params.slug)
return { post }
}
})
function BlogPost() {
const { post } = Route.useLoaderData()
// Rendered on server, then hydrated on client
return {post.content}
}
Selective SSR Control: Fine-tune which routes use SSR:
// routes/admin.tsx - client-side only for admin dashboard
export const Route = createFileRoute('/admin')({
component: AdminDashboard,
ssr: false, // Skip server rendering
loader: clientOnly(() => {
// This loader only runs on client
return getClientState()
})
})
Streaming Responses: Stream components as they load data:
import { Suspense } from 'react'
import { Await } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
component: Dashboard,
loader: async () => {
// Start both requests in parallel
const analyticsPromise = fetchAnalytics()
const recentActivityPromise = fetchRecentActivity()
return {
// Return promises, don't await yet
analytics: analyticsPromise,
recentActivity: recentActivityPromise
}
}
})
function Dashboard() {
const { analytics, recentActivity } = Route.useLoaderData()
return (
}>
{/* Stream analytics when ready */}
{(data) => }
}>
{/* Stream activity independently */}
{(data) => }
)
}
This streaming approach means the server can start sending the HTML shell immediately, stream the analytics chart when that data loads, and stream the activity feed when it's ready—all without waiting for everything to complete.
Middleware and Server Context
Middleware in TanStack Start provides a composable way to handle cross-cutting concerns like authentication, logging, rate limiting, and context injection:
Request Middleware (runs for all requests):
// src/middleware/request.ts
import { createMiddleware } from '@tanstack/start'
export const requestLogger = createMiddleware().server(async ({ next, request }) => {
const start = Date.now()
console.log([${request.method}] ${request.url})
const response = await next()
const duration = Date.now() - start
console.log([${request.method}] ${request.url} - ${duration}ms)
return response
})
// Apply globally in src/start.ts
export const start = createStart({
requestMiddleware: [requestLogger]
})
Authentication Middleware:
// src/middleware/auth.ts
import { createMiddleware } from '@tanstack/start'
import { verifyToken } from './auth-utils'
export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new Response('Unauthorized', { status: 401 })
}
const user = await verifyToken(token)
if (!user) {
throw new Response('Invalid token', { status: 401 })
}
// Inject user into context for downstream handlers
return next({
context: {
user
}
})
})
// Use in server functions
export const getMyData = createServerFn('GET', async (ctx) => {
// ctx.user available thanks to middleware
return fetchUserData(ctx.user.id)
}).middleware([authMiddleware])
Rate Limiting Middleware:
import { createMiddleware } from '@tanstack/start'
import { RateLimiter } from './rate-limiter'
const limiter = new RateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per window
})
export const rateLimitMiddleware = createMiddleware().server(async ({ next, request }) => {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const { allowed, remaining } = await limiter.check(ip)
if (!allowed) {
throw new Response('Too many requests', {
status: 429,
headers: {
'Retry-After': '900'
}
})
}
const response = await next()
// Add rate limit headers
response.headers.set('X-RateLimit-Remaining', remaining.toString())
return response
})
Composing Multiple Middleware:
export const protectedServerFn = createServerFn('POST', async (data, ctx) => {
// All middleware has run at this point
// ctx.user exists from authMiddleware
// request has been logged by requestLogger
// rate limit has been checked
return processProtectedAction(data, ctx.user)
}).middleware([
rateLimitMiddleware,
authMiddleware,
requestLogger
])
Server Routes and API Endpoints
Beyond page routes, TanStack Start supports dedicated server routes for API endpoints:
JSON API Routes:
// routes/api/users.ts
import { createServerRoute } from '@tanstack/start'
export const Route = createServerRoute('/api/users', {
GET: async ({ request }) => {
const url = new URL(request.url)
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '10')
const users = await db.users.findMany({
skip: (page - 1) * limit,
take: limit
})
return Response.json({
users,
page,
hasMore: users.length === limit
})
},
POST: async ({ request }) => {
const data = await request.json()
// Validate data
if (!data.email || !data.name) {
return Response.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
const user = await db.users.create({ data })
return Response.json({ user }, { status: 201 })
}
})
File Upload Handling:
// routes/api/upload.ts
export const Route = createServerRoute('/api/upload', {
POST: async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return Response.json({ error: 'No file provided' }, { status: 400 })
}
// Process file upload
const buffer = await file.arrayBuffer()
const savedPath = await saveToStorage(buffer, file.name)
return Response.json({
success: true,
url: savedPath
})
}
})
Webhook Endpoints:
// routes/api/webhooks/stripe.ts
export const Route = createServerRoute('/api/webhooks/stripe', {
POST: async ({ request }) => {
const sig = request.headers.get('stripe-signature')
const body = await request.text()
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
)
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object)
break
case 'payment_intent.failed':
await handlePaymentFailure(event.data.object)
break
}
return new Response(null, { status: 200 })
}
})
Content Security Policy and Nonce Support
TanStack Start includes built-in support for Content Security Policy (CSP) with automatic nonce generation for enhanced security:
// src/start.ts
export const start = createStart({
csp: {
enabled: true,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", "'nonce-{NONCE}'"],
'style-src': ["'self'", "'nonce-{NONCE}'"],
'img-src': ["'self'", 'data:', 'https:'],
'connect-src': ["'self'", 'https://api.example.com']
}
}
})
// Nonces are automatically applied to inline scripts and styles
// Server automatically replaces {NONCE} with a unique per-request nonce
Zero-JS Rendering
For maximum performance and accessibility, TanStack Start supports rendering pages that work without JavaScript:
// routes/blog/[slug].tsx
export const Route = createFileRoute('/blog/$slug')({
component: BlogPost,
loader: async ({ params }) => {
const post = await fetchPost(params.slug)
return { post }
},
// Works fully server-rendered without JS hydration
clientLoader: undefined // Explicitly disable client-side data loading
})
function BlogPost() {
const { post } = Route.useLoaderData()
// Pure HTML, works with JS disabled
return (
{post.title}
)
}
Getting Started with TanStack Start
Installation and Project Setup
Create a new TanStack Start project using the official CLI:
Create new project
npx create-tanstack-start@latest my-app
Or with pnpm
pnpm create tanstack-start my-app
Or with yarn
yarn create tanstack-start my-app
The CLI prompts for configuration choices:
- •TypeScript or JavaScript
- •Styling solution (Tailwind, CSS Modules, styled-components, etc.)
- •Testing framework (Vitest, Jest)
- •Deployment target (Vercel, Netlify, Node, Docker)
Manual Installation:
Initialize npm project
npm init -y
Install dependencies
npm install @tanstack/start @tanstack/react-router@latest vite
Install dev dependencies
npm install -D @tanstack/start-vite-plugin @types/node
Project Structure:
my-app/
├── src/
│ ├── routes/ # File-based routing
│ │ ├── index.tsx # Home page (/)
│ │ ├── about.tsx # About page (/about)
│ │ └── blog/
│ │ ├── index.tsx # Blog home (/blog)
│ │ └── [slug].tsx # Blog post (/blog/[slug])
│ ├── start.ts # Start configuration
│ └── entry-client.tsx # Client entry point
├── public/ # Static assets
├── vite.config.ts # Vite configuration
└── package.json
Basic Configuration
vite.config.ts:
import { defineConfig } from 'vite'
import tanstackStart from '@tanstack/start-vite-plugin'
export default defineConfig({
plugins: [
tanstackStart({
// Enable server-side rendering
ssr: true,
// Deployment target
target: 'node', // or 'vercel', 'netlify', 'cloudflare'
})
]
})
src/start.ts:
import { createStart } from '@tanstack/start'
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
// Create router from file-based routes
const router = createRouter({
routeTree,
defaultPreload: 'intent', // Preload on hover/focus
})
// Create Start instance
export const start = createStart({
router,
// Optional configuration
requestMiddleware: [],
ssr: {
enabled: true,
streaming: true
}
})
// Type augmentation for TypeScript
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
src/entry-client.tsx:
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { start } from './start'
// Hydrate the server-rendered HTML
hydrateRoot(
document.getElementById('root')!,
)
Creating Your First Route
src/routes/index.tsx (Home page):
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'
// Server function to load data
const getWelcomeMessage = createServerFn('GET', async () => {
// Server-only code
return {
message: 'Welcome to TanStack Start!',
timestamp: new Date().toISOString()
}
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => {
const data = await getWelcomeMessage()
return data
}
})
function Home() {
const data = Route.useLoaderData()
return (
{data.message}
Loaded at: {data.timestamp}
)
}
src/routes/about.tsx:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: About
})
function About() {
return (
About Us
This is a TanStack Start application
)
}
Dynamic Routes - src/routes/users/[id].tsx:
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'
const fetchUser = createServerFn('GET', async (userId: string) => {
// Fetch from database
const user = await db.users.findUnique({ where: { id: userId } })
if (!user) throw new Error('User not found')
return user
})
export const Route = createFileRoute('/users/$id')({
component: UserProfile,
loader: async ({ params }) => {
// params.id is fully typed!
const user = await fetchUser(params.id)
return { user }
}
})
function UserProfile() {
const { user } = Route.useLoaderData()
return (
{user.name}
Email: {user.email}
)
}
Development Workflow
Start development server:
npm run dev
This starts Vite dev server with:
- •Hot module replacement
- •Fast refresh for instant updates
- •Type checking in watch mode
- •Server function hot reload
Build for production:
npm run build
Creates optimized production bundles with:
- •Code splitting per route
- •Server/client code separation
- •Tree shaking and minification
- •Static asset optimization
Preview production build:
npm run preview
Advanced Use Cases
Building a Full-Stack Blog Platform
Here's a complete example of a blog with authentication, data mutations, and SSR:
Database Setup (Prisma):
// prisma/schema.prisma
model Post {
id String @id @default(uuid())
title String
slug String @unique
content String
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id String @id @default(uuid())
email String @unique
name String
posts Post[]
}
Server Functions:
// src/lib/blog.server.ts
import { createServerFn } from '@tanstack/start'
import { prisma } from './prisma'
export const getPosts = createServerFn('GET', async () => {
return prisma.post.findMany({
where: { published: true },
include: { author: true },
orderBy: { createdAt: 'desc' }
})
})
export const getPost = createServerFn('GET', async (slug: string) => {
const post = await prisma.post.findUnique({
where: { slug },
include: { author: true }
})
if (!post) {
throw new Error('Post not found')
}
return post
})
export const createPost = createServerFn('POST', async (
data: { title: string; content: string; slug: string },
ctx
) => {
// Assume auth middleware provides ctx.user
return prisma.post.create({
data: {
...data,
authorId: ctx.user.id
}
})
}).middleware([authMiddleware])
export const updatePost = createServerFn('POST', async (
data: { id: string; title: string; content: string },
ctx
) => {
// Verify ownership
const post = await prisma.post.findUnique({ where: { id: data.id } })
if (post.authorId !== ctx.user.id) {
throw new Error('Unauthorized')
}
return prisma.post.update({
where: { id: data.id },
data: {
title: data.title,
content: data.content,
updatedAt: new Date()
}
})
}).middleware([authMiddleware])
Blog List Route:
// src/routes/blog/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getPosts } from '../../lib/blog.server'
export const Route = createFileRoute('/blog')({
component: BlogList,
loader: async () => {
const posts = await getPosts()
return { posts }
}
})
function BlogList() {
const { posts } = Route.useLoaderData()
return (
Blog Posts
{posts.map(post => (
-
{post.title}
By {post.author.name}
))}
)
}
Blog Post Route with Streaming Comments:
// src/routes/blog/[slug].tsx
import { createFileRoute } from '@tanstack/react-router'
import { Suspense } from 'react'
import { Await } from '@tanstack/react-router'
import { getPost, getComments } from '../../lib/blog.server'
export const Route = createFileRoute('/blog/$slug')({
component: BlogPost,
loader: async ({ params }) => {
// Load post immediately
const post = await getPost(params.slug)
// Start loading comments but don't await
const commentsPromise = getComments(post.id)
return {
post,
commentsPromise
}
}
})
function BlogPost() {
const { post, commentsPromise } = Route.useLoaderData()
return (
{post.title}
By {post.author.name}
Comments
}>
{(comments) => }
)
}
E-commerce Product Catalog with Search
Product Search Server Function:
// src/lib/products.server.ts
import { createServerFn } from '@tanstack/start'
import { z } from 'zod'
const searchSchema = z.object({
query: z.string().optional(),
category: z.string().optional(),
minPrice: z.number().optional(),
maxPrice: z.number().optional(),
page: z.number().default(1),
limit: z.number().default(20)
})
export const searchProducts = createServerFn('GET', async (params: unknown) => {
// Validate search parameters
const validated = searchSchema.parse(params)
const filters: any = {}
if (validated.query) {
filters.OR = [
{ name: { contains: validated.query, mode: 'insensitive' } },
{ description: { contains: validated.query, mode: 'insensitive' } }
]
}
if (validated.category) {
filters.category = validated.category
}
if (validated.minPrice || validated.maxPrice) {
filters.price = {
...(validated.minPrice && { gte: validated.minPrice }),
...(validated.maxPrice && { lte: validated.maxPrice })
}
}
const [products, total] = await Promise.all([
prisma.product.findMany({
where: filters,
skip: (validated.page - 1) * validated.limit,
take: validated.limit,
include: { images: true }
}),
prisma.product.count({ where: filters })
])
return {
products,
pagination: {
page: validated.page,
limit: validated.limit,
total,
pages: Math.ceil(total / validated.limit)
}
}
})
Product Search Route with Search Params:
// src/routes/products/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { searchProducts } from '../../lib/products.server'
export const Route = createFileRoute('/products')({
component: ProductCatalog,
validateSearch: (search) => ({
query: search.query as string | undefined,
category: search.category as string | undefined,
minPrice: search.minPrice ? Number(search.minPrice) : undefined,
maxPrice: search.maxPrice ? Number(search.maxPrice) : undefined,
page: search.page ? Number(search.page) : 1
}),
loader: async ({ search }) => {
const results = await searchProducts(search)
return results
}
})
function ProductCatalog() {
const searchParams = Route.useSearch()
const { products, pagination } = Route.useLoaderData()
const navigate = Route.useNavigate()
const updateSearch = (updates: Partial) => {
navigate({
search: (prev) => ({ ...prev, ...updates, page: 1 }) // Reset to page 1 on new search
})
}
return (
updateSearch({ page })}
/>
)
}
Real-Time Dashboard with Polling
// src/routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { getDashboardMetrics } from '../lib/metrics.server'
export const Route = createFileRoute('/dashboard')({
component: Dashboard,
loader: async () => {
// Initial data for SSR
const initialMetrics = await getDashboardMetrics()
return { initialMetrics }
}
})
function Dashboard() {
const { initialMetrics } = Route.useLoaderData()
// Client-side polling for real-time updates
const { data: metrics } = useQuery({
queryKey: ['dashboard-metrics'],
queryFn: () => getDashboardMetrics(),
initialData: initialMetrics,
refetchInterval: 30000, // Poll every 30 seconds
refetchIntervalInBackground: true
})
return (
Dashboard
$${metrics.revenueToday}} />
${metrics.conversionRate}%} />
)
}
Best Practices
Code Organization
Separate Server and Client Code:
src/
├── lib/
│ ├── server/ # Server-only utilities
│ │ ├── db.ts
│ │ ├── auth.ts
│ │ └── email.ts
│ ├── client/ # Client-only utilities
│ │ ├── analytics.ts
│ │ └── storage.ts
│ └── shared/ # Isomorphic code
│ ├── validation.ts
│ └── constants.ts
├── routes/ # File-based routes
└── components/ # React components
Use .server.ts suffix for server modules:
// lib/database.server.ts - never bundled for client
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()
// lib/api.server.ts
export const apiClient = createAPIClient({
apiKey: process.env.SECRET_API_KEY // Safe on server only
})
Type Safety Best Practices
Define shared types:
// types/models.ts
export interface User {
id: string
email: string
name: string
}
export interface Post {
id: string
title: string
slug: string
content: string
authorId: string
}
// Use in server functions
import type { User, Post } from '../types/models'
export const getUser = createServerFn('GET', async (id: string): Promise => {
return prisma.user.findUnique({ where: { id } })
})
Validate inputs with Zod:
import { z } from 'zod'
import { createServerFn } from '@tanstack/start'
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
slug: z.string().regex(/^[a-z0-9-]+$/)
})
export const createPost = createServerFn('POST', async (input: unknown) => {
// Validate and parse input
const data = createPostSchema.parse(input)
return prisma.post.create({ data })
})
Error Handling
Create custom error components:
// components/ErrorBoundary.tsx
export function ErrorBoundary({ error }: { error: Error }) {
if (error.message === 'Not found') {
return
}
if (error.message === 'Unauthorized') {
return
}
return (
Something went wrong
{error.message}
)
}
// Use in routes
export const Route = createFileRoute('/protected')({
component: ProtectedPage,
errorComponent: ErrorBoundary,
// ...
})
Handle server function errors gracefully:
export const updateProfile = createServerFn('POST', async (data, ctx) => {
try {
return await prisma.user.update({
where: { id: ctx.user.id },
data
})
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation
throw new Error('Email already in use')
}
throw new Error('Failed to update profile')
}
})
Performance Optimization
Implement route-level code splitting:
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
}>
)
}
Use route preloading:
import { Link } from '@tanstack/react-router'
// Preload on hover (default with router.defaultPreload = 'intent')
Products
// Explicit preload
Products
// Preload immediately
Products
Optimize bundle size:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks
'react-vendor': ['react', 'react-dom'],
'tanstack': ['@tanstack/react-router', '@tanstack/react-query']
}
}
}
}
})
Comparison with Alternatives
TanStack Start vs. Next.js
Next.js Strengths:
- •Mature ecosystem with extensive plugins and integrations
- •Vercel platform optimization (Edge Runtime, ISR, Image Optimization)
- •Larger community and more learning resources
- •Built-in image and font optimization
- •Server Components support (App Router)
TanStack Start Advantages:
- •Superior type safety with full inference across server/client boundary
- •Unified route tree (no separate
appandapidirectories) - •Client-first DX while maintaining full server capabilities
- •Framework-agnostic (not tied to specific hosting platform)
- •More transparent implementation, easier to debug
- •No vendor lock-in or corporate control
When to choose Next.js: Enterprise teams heavily invested in Vercel ecosystem, projects requiring Server Components, teams prioritizing ecosystem size over DX.
When to choose TanStack Start: Teams prioritizing type safety and DX, multi-platform deployment requirements, desire for framework independence, projects where understanding the full stack matters.
TanStack Start vs. Remix
Remix Strengths:
- •Progressive enhancement philosophy
- •Excellent form handling with nested routes
- •Built-in error and loading boundaries
- •Strong focus on web fundamentals
- •Mature and production-proven
TanStack Start Advantages:
- •Better TypeScript experience with full type inference
- •More flexible server function model (not tied to loaders/actions)
- •Built on Vite (faster dev experience)
- •Lighter weight and more modular
- •Independent and community-driven (vs. Shopify-owned)
When to choose Remix: Teams building form-heavy applications, strong preference for progressive enhancement, comfortable with framework-specific patterns.
When to choose TanStack Start: TypeScript-first teams, projects requiring maximum flexibility, preference for Vite tooling, desire for framework independence.
TanStack Start vs. Astro
Astro Strengths:
- •Multi-framework support (React, Vue, Svelte, etc.)
- •Content-focused sites (blogs, marketing pages)
- •Islands architecture for minimal JavaScript
- •Built-in Markdown/MDX support
- •Excellent for static sites
TanStack Start Advantages:
- •Better for dynamic, data-driven applications
- •Full-stack React experience
- •More sophisticated routing and state management
- •Superior type safety for server/client interactions
- •Better suited for SPAs with heavy client-side interactivity
When to choose Astro: Content-heavy sites, marketing pages, blogs, projects using multiple frameworks, sites that are mostly static.
When to choose TanStack Start: React-only projects, applications requiring complex client-side state, data-driven dashboards, e-commerce platforms.
Production Deployment
Deploying to Vercel
Install Vercel CLI
npm i -g vercel
Deploy
vercel
Production deployment
vercel --prod
vercel.json configuration:
{
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"framework": "tanstack-start"
}
Deploying to Netlify
Install Netlify CLI
npm i -g netlify-cli
Deploy
netlify deploy
Production
netlify deploy --prod
netlify.toml:
[build]
command = "npm run build"
publish = "dist/client"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Deploying to Node.js Server
// server.ts
import { createServer } from '@tanstack/start/server'
import { start } from './src/start'
const server = createServer({
start,
port: process.env.PORT || 3000
})
server.listen(() => {
console.log(Server running on port ${server.address().port})
})
Docker deployment:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "server.js"]
Environment Variables
// src/env.ts
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
NODE_ENV: z.enum(['development', 'production', 'test'])
})
export const env = envSchema.parse(process.env)
Conclusion
TanStack Start v1 Release Candidate represents a refreshing approach to full-stack React development. By prioritizing developer experience, type safety, and transparency over framework magic and vendor lock-in, it offers a compelling alternative to the current generation of meta-frameworks.
The unified route tree architecture eliminates the cognitive overhead of context-switching between different routing paradigms. Type-safe server functions provide the security of RPC without sacrificing the flexibility of REST. Middleware composition enables elegant cross-cutting concerns without framework-specific patterns. And the entire system is built on proven, transparent technologies (Vite, Vinxi, Nitro) that developers can understand and debug.
For teams frustrated by the limitations of existing frameworks—whether it's Next.js's Vercel coupling, Remix's opinionated patterns, or the general complexity of assembling modern full-stack applications—TanStack Start offers a path forward that doesn't compromise on capability, performance, or developer joy.
As the framework progresses from Release Candidate to stable v1, it's poised to become a major player in the React ecosystem, backed by the proven track record of the TanStack team and the passionate community that has made TanStack Query, Router, and Table indispensable tools for millions of developers.
The future of full-stack React development is type-safe, transparent, and independent. TanStack Start is leading the way.
Additional Resources
- •Official Documentation: https://tanstack.com/start/latest/docs
- •GitHub Repository: https://github.com/TanStack/router (monorepo includes Start)
- •Discord Community: https://tlinz.com/discord
- •Release Announcement: https://tanstack.com/blog/announcing-tanstack-start-v1
- •Example Applications: https://github.com/TanStack/router/tree/main/examples/react/start-basic
- •TypeScript Examples: https://tanstack.com/start/latest/docs/framework/react/examples
- •Migration Guides: https://tanstack.com/start/latest/docs/framework/react/migration
- •API Reference: https://tanstack.com/start/latest/docs/framework/react/api
- •TanStack Router Docs: https://tanstack.com/router/latest