Not every React app wants to be a Next.js app. TanStack Start — the full-stack framework from the team behind TanStack Query and Router — went stable in early 2026 and has quietly become a real alternative. It is a different set of trade-offs: no Server Components, but end-to-end type safety, Vite speed, and a much thinner runtime.
This article covers what Start actually is, how it compares to Next.js on the things that matter, and a working starter example.
What TanStack Start Is
Start is a Vite-based React framework built on:
- TanStack Router — file-based routing with fully typed params and search params
- Vite — for dev server, HMR, and bundling
- Server functions — typed RPC-ish functions that run on the server, callable from components
- TanStack Query — integrated for data loading, with SSR prefetch and dehydration
Notably absent: React Server Components. Start uses traditional client-side React with SSR. If you find the RSC mental model awkward, or you are building a highly interactive app where the server/client boundary does not map cleanly to RSC patterns, Start might feel more natural.
Setting Up a Project
Terminal
pnpm create @tanstack/start@latest my-app
cd my-app
pnpm install
pnpm dev
Dev server boots in under two seconds on a warm machine. HMR is Vite, so it is fast even on large codebases.
File-Based Routing with Typed Params
app/routes/posts/$slug.tsx
import { createFileRoute } from '@tanstack/react-router';
import { getPost } from '~/server/posts';
export const Route = createFileRoute('/posts/$slug')({
loader: ({ params }) => getPost({ data: params.slug }),
component: PostPage,
});
function PostPage() {
const post = Route.useLoaderData();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
The $slug in the filename becomes a typed parameter. Route.useLoaderData() is fully typed from the loader's return type — no generics, no codegen. If you rename the file to $id.tsx, TypeScript immediately tells every Link pointing at /posts/$slug to update.
Server Functions
Server functions are Start's answer to API routes and server actions. You define a function, mark it server-only, and call it from anywhere — it serializes its arguments, runs on the server, and returns the result.
app/server/posts.ts
import { createServerFn } from '@tanstack/react-start';
import { z } from 'zod';
import { db } from '~/lib/db';
export const getPost = createServerFn({ method: 'GET' })
.validator(z.string())
.handler(async ({ data: slug }) => {
const post = await db.posts.findFirst({ where: { slug } });
if (!post) throw new Error('Not found');
return post;
});
export const createPost = createServerFn({ method: 'POST' })
.validator(
z.object({ title: z.string(), content: z.string(), slug: z.string() }),
)
.handler(async ({ data }) => {
return db.posts.create({ data });
});
The .validator() call parses input with Zod at the boundary. In components, createPost({ data: {...} }) is a regular function call that happens to run on the server — no fetch, no route file, no manual typing of request/response shapes.
SSR and Prefetching
Loaders run on the server during SSR and produce HTML with data inlined. On navigation, loaders run on the client. TanStack Query integration means you can also prefetch: during SSR, dehydrate the query cache; on hydration, it is already populated.
Prefetching with TanStack Query
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { getPost } from '~/server/posts';
const postQuery = (slug: string) =>
queryOptions({
queryKey: ['post', slug],
queryFn: () => getPost({ data: slug }),
});
export const Route = createFileRoute('/posts/$slug')({
loader: ({ context, params }) =>
context.queryClient.ensureQueryData(postQuery(params.slug)),
component: PostPage,
});
function PostPage() {
const { slug } = Route.useParams();
const { data: post } = useSuspenseQuery(postQuery(slug));
return <article>{post.title}</article>;
}
Pro Tip: Putting the
queryOptions()call outside the route file lets you share the same query definition between server loader, client component, and tests. It is a clean pattern that TanStack's types fully support.
How It Compares to Next.js
- Rendering model — Next uses RSC by default. Start uses traditional SSR + client React.
- Type safety — Start's type inference (routes, params, server functions) is stronger out of the box. Next is catching up but still relies on more generics.
- Dev speed — Both are fast. Vite-based Start feels slightly snappier on cold start; Turbopack in Next 16 is comparable on HMR.
- Deployment — Start deploys to Vercel, Netlify, Cloudflare, Bun, Node, and static export. Next is most at home on Vercel but runs anywhere.
- Community/ecosystem — Next is still the 500-pound gorilla. Most blog posts, Stack Overflow answers, and hireable candidates know Next first.
When to Pick TanStack Start
- You want strong type safety across routes, params, and server calls without codegen
- You're building a highly interactive SPA-like app where RSC does not add much
- You already use TanStack Query heavily and want first-class integration
- You want Vite's dev experience
When to Stay on Next.js
- You want RSC, Server Actions, and the Vercel-integrated deployment story
- You need Next-specific features (Image optimization with ISR, Middleware, Cache Components)
- Your team and hiring pipeline is Next-shaped
Neither choice is wrong. Start has earned a real seat at the table, but it is complementary to Next rather than a replacement.
