Fix: TanStack Start Not Working — Server Functions Failing, Routes Not Loading, or SSR Errors
Quick Answer
How to fix TanStack Start issues — project setup, file-based routing, server functions with createServerFn, data loading, middleware, SSR hydration, and deployment configuration.
The Problem
A TanStack Start route renders on the server but hydration fails:
Error: Hydration mismatch — server rendered HTML doesn't match clientOr server functions return undefined:
const serverFn = createServerFn('GET', async () => {
return { users: await db.query.users.findMany() };
});
// Returns undefined on the clientOr the dev server crashes on startup:
Error: Cannot find module 'vinxi' — or —
Error: Route tree is emptyWhy This Happens
TanStack Start is a full-stack React framework built on TanStack Router and Vinxi. It’s newer than Next.js and has a different architecture:
- TanStack Start uses Vinxi as its build system — Vinxi orchestrates the server, client, and SSR bundles. If Vinxi isn’t properly configured or installed, the dev server can’t start.
- Server functions use
createServerFn— these are RPC-like functions that run on the server and are callable from the client. They must be defined in files that the server bundle includes. Using them in client-only files causes import errors. - Routing is powered by TanStack Router — the same file-based routing from
@tanstack/react-routerapplies. The route tree must be generated before routes work. - SSR is on by default — every route renders on the server first. Client-only code (browser APIs,
window,document) causes hydration mismatches if not guarded.
Fix 1: Project Setup
npm create @tanstack/start@latest my-app
cd my-app
npm install
npm run dev// app.config.ts — TanStack Start configuration
import { defineConfig } from '@tanstack/start/config';
export default defineConfig({
server: {
preset: 'node-server', // 'node-server' | 'vercel' | 'netlify' | 'cloudflare-pages'
},
});// app/router.tsx — router configuration
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export function createRouter() {
return createTanStackRouter({
routeTree,
defaultPreload: 'intent',
});
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>;
}
}// app/routes/__root.tsx — root layout
import { createRootRoute, Outlet, ScrollRestoration } from '@tanstack/react-router';
import { Meta, Scripts } from '@tanstack/start';
export const Route = createRootRoute({
component: RootComponent,
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'My App' },
],
}),
});
function RootComponent() {
return (
<html lang="en">
<head>
<Meta />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}Fix 2: Server Functions
// app/server/db.ts — server-only code
import { createServerFn } from '@tanstack/start';
// GET server function — fetches data
export const getUsers = createServerFn('GET', async () => {
const users = await db.query.users.findMany({
orderBy: desc(users.createdAt),
limit: 50,
});
return users;
});
// POST server function — mutations
export const createUser = createServerFn('POST', async (input: { name: string; email: string }) => {
const [user] = await db.insert(users).values(input).returning();
return user;
});
// Server function with validation
import { z } from 'zod';
const updateSchema = z.object({
id: z.string(),
name: z.string().min(1),
email: z.string().email(),
});
export const updateUser = createServerFn('POST', async (rawInput: unknown) => {
const input = updateSchema.parse(rawInput);
const [user] = await db.update(users).set(input).where(eq(users.id, input.id)).returning();
return user;
});
// Server function with request context
export const getCurrentUser = createServerFn('GET', async (_, ctx) => {
const token = ctx.request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) throw new Error('Unauthorized');
const user = await verifyTokenAndGetUser(token);
return user;
});Fix 3: Route Data Loading
// app/routes/posts.tsx — route with loader
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
const getPosts = createServerFn('GET', async () => {
return db.query.posts.findMany({
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
});
});
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await getPosts();
return { posts };
},
component: PostsPage,
});
function PostsPage() {
const { posts } = Route.useLoaderData();
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to="/posts/$postId" params={{ postId: post.id }}>
{post.title}
</Link>
</li>
))}
</ul>
</div>
);
}
// app/routes/posts/$postId.tsx — dynamic route
const getPost = createServerFn('GET', async (postId: string) => {
const post = await db.query.posts.findFirst({ where: eq(posts.id, postId) });
if (!post) throw notFound();
return post;
});
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await getPost(params.postId);
return { post };
},
component: PostPage,
notFoundComponent: () => <div>Post not found</div>,
});
function PostPage() {
const { post } = Route.useLoaderData();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}Fix 4: Forms and Mutations
// app/routes/posts.new.tsx
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
import { useState } from 'react';
const createPost = createServerFn('POST', async (input: { title: string; body: string }) => {
const [post] = await db.insert(posts).values({
...input,
published: false,
authorId: 'current-user',
}).returning();
return post;
});
export const Route = createFileRoute('/posts/new')({
component: NewPostPage,
});
function NewPostPage() {
const navigate = useNavigate();
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError('');
const formData = new FormData(e.currentTarget);
const title = formData.get('title') as string;
const body = formData.get('body') as string;
try {
const post = await createPost({ title, body });
navigate({ to: '/posts/$postId', params: { postId: post.id } });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create post');
} finally {
setLoading(false);
}
}
return (
<div>
<h1>New Post</h1>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Write your post..." required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
</form>
</div>
);
}Fix 5: Middleware and Authentication
// app/middleware.ts — server middleware
import { createMiddleware } from '@tanstack/start';
export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
let user = null;
if (token) {
try {
user = await verifyToken(token);
} catch {
// Invalid token — continue as unauthenticated
}
}
return next({ context: { user } });
});
// Use in server functions
export const getProfile = createServerFn('GET', async (_, ctx) => {
// ctx contains middleware context
if (!ctx.context.user) {
throw new Error('Unauthorized');
}
return ctx.context.user;
}).middleware([authMiddleware]);
// Protected route
export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => {
const user = await getProfile();
if (!user) throw redirect({ to: '/login' });
return { user };
},
component: DashboardPage,
});Fix 6: Deployment
// app.config.ts — deployment presets
// Vercel
export default defineConfig({
server: { preset: 'vercel' },
});
// Netlify
export default defineConfig({
server: { preset: 'netlify' },
});
// Cloudflare Pages
export default defineConfig({
server: { preset: 'cloudflare-pages' },
});
// Node.js server
export default defineConfig({
server: { preset: 'node-server' },
});# Build
npm run build
# Start production server (node-server preset)
node .output/server/index.mjsStill Not Working?
“Cannot find module vinxi” — run npm install to ensure all dependencies are installed. TanStack Start depends on Vinxi internally. If the error persists, delete node_modules and package-lock.json, then reinstall.
Route tree is empty — route files must be in app/routes/ and export const Route = createFileRoute(...). Run the dev server to auto-generate routeTree.gen.ts. Check that filenames follow the convention: index.tsx, posts.tsx, posts.$postId.tsx.
Server function returns undefined — ensure the function returns a value. createServerFn('GET', async () => { db.query... }) without return produces undefined. Also check the function is imported correctly — server functions must be called, not passed as references.
Hydration mismatch — code that accesses window, document, or localStorage runs on the server during SSR. Guard browser-only code with typeof window !== 'undefined' checks or use useEffect for client-only logic.
For related framework issues, see Fix: TanStack Router Not Working and Fix: Next.js App Router Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Waku Not Working — Server Components Not Rendering, Client Components Not Interactive, or Build Errors
How to fix Waku React framework issues — RSC setup, server and client component patterns, data fetching, routing, layout system, and deployment configuration.
Fix: Wasp Not Working — Build Failing, Auth Not Working, or Operations Returning Empty
How to fix Wasp full-stack framework issues — main.wasp configuration, queries and actions, authentication setup, Prisma integration, job scheduling, and deployment.
Fix: Next.js Server Action Not Working — Action Not Called or Returns Error
How to fix Next.js Server Actions — use server directive, form binding, revalidation, error handling, middleware conflicts, and client component limitations.
Fix: Analog Not Working — Routes Not Loading, API Endpoints Failing, or Vite Build Errors
How to fix Analog (Angular meta-framework) issues — file-based routing, API routes with Nitro, content collections, server-side rendering, markdown pages, and deployment.