TanStack Start: Full Stack TypeScript with TanStack Router

I have been using TanStack Router in a project for the past few months, and when TanStack Start (the full stack layer on top of the router) reached a stable enough point to try, I jumped in. Tanner Linsley and team have built something that takes type safety in routing to a level I did not think was possible.
Why TanStack Router
Before talking about Start, it is worth understanding why TanStack Router exists. React Router and Next.js both have routing solutions, but they trade off type safety for convenience. Dynamic route parameters are typed as string | string[]. Search parameters are untyped. You find out about broken links at runtime, not compile time.
TanStack Router makes every part of a route type-safe. Path parameters, search parameters, loader data, and even the links you render in your components. If a route expects a search parameter called page with a number type, linking to that route without providing page (or providing it as a string) is a TypeScript error.
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
export const Route = createFileRoute('/posts')({
validateSearch: z.object({
page: z.number().default(1),
sort: z.enum(['date', 'title']).default('date'),
}),
loader: async ({ search }) => {
// search.page is typed as number
// search.sort is typed as 'date' | 'title'
const posts = await fetchPosts(search.page, search.sort);
return { posts };
},
component: PostsPage,
});
The validateSearch option uses Zod (or any validation library) to define and validate search parameters. The types flow through to the loader, the component, and any link that points to this route.
File Based Routing
TanStack Start uses file-based routing similar to Next.js, but with a twist: the route tree is generated as TypeScript code, which means the type inference works at compile time rather than relying on runtime conventions.
The file structure maps directly to routes:
src/routes/
__root.tsx # Root layout
index.tsx # /
about.tsx # /about
posts/
index.tsx # /posts
$postId.tsx # /posts/:postId
The $ prefix denotes a dynamic segment. The generated route tree provides full type inference for every route, including nested layouts and parameter types.
Server Functions
TanStack Start adds server functions for data loading and mutations. These run on the server and are called transparently from the client.
import { createServerFn } from '@tanstack/start';
const getPost = createServerFn('GET', async (postId: string) => {
const post = await db.posts.findUnique({
where: { id: postId },
});
if (!post) throw new Error('Post not found');
return post;
});
const createPost = createServerFn('POST', async (data: {
title: string;
content: string;
}) => {
return db.posts.create({ data });
});
Server functions are type-safe end to end. The parameter types and return types flow from the server function definition to wherever you call it on the client. No separate API layer, no manual type definitions, no code generation.
Loaders and Actions
Each route can define a loader (for reading data) and beforeLoad (for guards and redirects). The loader data is available in the component through a typed hook.
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// params.postId is typed as string
const post = await getPost(params.postId);
return { post };
},
component: PostDetail,
});
function PostDetail() {
const { post } = Route.useLoaderData();
// post is fully typed based on the loader return
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
The Route.useLoaderData() hook returns exactly the type that the loader returns. If you change the loader's return shape, TypeScript immediately flags every component that uses the old shape.
Type Safe Links
This is the feature that I keep showing people because it is so satisfying. When you create a link to a route, TypeScript enforces the correct parameters.
import { Link } from '@tanstack/react-router';
// This is valid:
<Link to="/posts/$postId" params={{ postId: '123' }} />
// This is a type error (missing postId):
<Link to="/posts/$postId" />
// This is a type error (wrong param name):
<Link to="/posts/$postId" params={{ id: '123' }} />
// Search params are also enforced:
<Link
to="/posts"
search={{ page: 1, sort: 'date' }}
/>
Broken links become compile-time errors instead of runtime 404s. On a large application with dozens of routes, this is incredibly valuable.
Compared to Next.js and Remix
Next.js App Router has server components and server actions, but the routing is not type-safe. Dynamic params are string, search params are Record<string, string | string[]>, and there is no compile-time link validation.
Remix has a good loader/action pattern, but the types rely on inference from the loader function, and the link validation is not as strict.
TanStack Start takes type safety further than either of them. The tradeoff is ecosystem size (Next.js has far more community resources and deployment options) and maturity (TanStack Start is newer and still evolving).
My Experience
The developer experience around types is genuinely best in class. The moment you define a route, every link, every loader, every parameter is typed. The feedback loop is immediate. I catch route errors in my editor before I even save the file.
The framework is still young, so documentation gaps exist and some patterns are not yet well established. If you are building a production application today and need the broadest ecosystem support, Next.js is still the safer choice. But if type safety in your routing layer is a priority, TanStack Start is worth serious consideration. The quality of the TypeScript integration is something the other frameworks should aspire to.