Skip to content

Fix: TanStack Start Not Working — Server Functions Failing, Routes Not Loading, or SSR Errors

FixDevs ·

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 client

Or server functions return undefined:

const serverFn = createServerFn('GET', async () => {
  return { users: await db.query.users.findMany() };
});
// Returns undefined on the client

Or the dev server crashes on startup:

Error: Cannot find module 'vinxi' — or —
Error: Route tree is empty

Why 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-router applies. 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.mjs

Still 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles