Skip to main content
Dev ToolsBlog
HomeArticlesCategories

Dev Tools Blog

Modern development insights and cutting-edge tools for today's developers.

Quick Links

  • ArticlesView all development articles
  • CategoriesBrowse articles by category

Technologies

Built with Next.js 15, React 19, TypeScript, and Tailwind CSS.

© 2025 Dev Tools Blog. All rights reserved.

← Back to Home
frontend

TanStack Start v1 RC: The Future of Full-Stack React Development

Deep dive into TanStack Start v1 RC, the revolutionary full-stack React framework with unified routing, type-safe middleware, and Zero-JS rendering capabilities.

Published: 10/7/2025

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 (

{/* form fields */} {mutation.isPending && } {mutation.isError && } ) }

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}

    {new Date(post.createdAt).toLocaleDateString()}
  • ))}
) }

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 app and api directories)
  • •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

Key Features

  • ▸Unified Route Tree

    Single routing system without server-specific file duplication

  • ▸Type-Safe Server Functions

    End-to-end type safety from client to server with automatic inference

  • ▸Zero-JS Rendering

    Ship pages with no JavaScript for optimal performance

  • ▸Server Context

    Advanced server-side state management and middleware support

Related Links

  • Official Website ↗
  • GitHub Repository ↗
  • Documentation ↗