Hands-on with TanStack Start: First Steps and a Next.js Comparison
14 min readUpdated Feb 8, 2026
TL;DR
A practical, side-by-side look at TanStack Start's onboarding experience and how its routing, server functions, and caching model compare with Next.js. It focuses on TanStack Start, Next.js, React, and Routing so you can understand the main ideas, trade-offs, and practical context before reading the full article.
TanStack Start positions itself as a full-stack framework powered by TanStack Router and Vite, with full-document SSR, streaming, server functions, and API/server routes built in.
One important context note before you dive in: TanStack Start reached its v1 Release Candidate in September 2025 and remains in RC as of early 2026. The API is considered stable and feature-complete, but a stable 1.0 hasn't shipped yet. React Server Components support is planned as a non-breaking addition post-1.0.
TanStack Router puts type-safety at the center, including route params and search-param APIs. It also includes data-loading primitives (loaders) with caching built in.
That shifts the mindset a bit:
You think in route trees and data loaders first.
URL state (search params) feels like a typed store rather than string parsing.
You get a lot of compile-time safety at the cost of learning naming conventions.
Loaders are one of the first things you'll reach for. They're isomorphic — running server-side during SSR and client-side during navigation — and their return type flows directly into the component.
import { createFileRoute } from '@tanstack/react-router'export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => { // params.postId is typed as string const post = await fetchPost(params.postId) return { post } }, component: PostPage,})function PostPage() { // TypeScript infers { post: Post } from the loader const { post } = Route.useLoaderData() return <h1>{post.title}</h1>}
You can also run pre-checks with beforeLoad for things like auth guards:
The execution model: beforeLoad runs sequentially from outermost to innermost route, while loader functions run in parallel for sibling routes. Built-in SWR caching means loaders won't re-fetch unnecessarily.
Compare this with Next.js, where data fetching happens inside async Server Components or via fetch calls — there's no dedicated loader primitive, and type inference between the fetching layer and the component is something you manage yourself.
This is one of the features that genuinely felt like a step forward. In TanStack Router, search params are validated at the route level and fully typed everywhere — in <Link>, useSearch, and navigate.
import { z } from 'zod'import { zodValidator } from '@tanstack/zod-adapter'import { createFileRoute } from '@tanstack/react-router'const searchSchema = z.object({ query: z.string().optional(), category: z.enum(['electronics', 'clothing', 'books']).optional(), page: z.number().default(1),})export const Route = createFileRoute('/products')({ validateSearch: zodValidator(searchSchema), component: ProductsPage,})
Now every <Link> to this route gets type-checked search params:
In Next.js, useSearchParams() returns untyped strings. You can add Zod validation yourself, but it's not built into the routing layer. The difference is felt most on larger apps where search params drive significant UI state.
TanStack Start has a composable middleware system that attaches to server functions. This is one area where the design philosophy diverges significantly from Next.js.
import { createMiddleware } from '@tanstack/react-start'const authMiddleware = createMiddleware({ type: 'function' }) .server(async ({ next }) => { const session = await getSession() if (!session) throw redirect({ to: '/login' }) return next({ sendContext: { user: session.user } }) })// Attach to any server functionconst getProtectedData = createServerFn({ method: 'GET' }) .middleware([authMiddleware]) .handler(async ({ context }) => { // context.user is typed from the middleware return fetchUserData(context.user.id) })
Middleware can be composed — one middleware can depend on another, and context flows through the chain with full type safety.
In Next.js, middleware.ts runs on the Edge runtime and operates at the request level (before routing). It's powerful for redirects and rewrites but doesn't compose per-function or flow typed context into your handlers the way TanStack Start's middleware does.
Child route meta tags automatically override parent tags with the same name or property. The head function receives loader data, so dynamic meta is straightforward.
Next.js uses the metadata export or generateMetadata async function — a similar approach with different ergonomics. Next.js's generateMetadata has the advantage of automatic deduplication and a more declarative API, while Start's approach feels closer to "just return an array of tags."
One thing that surprised me was TanStack Start's selective SSR. You can configure SSR behavior per route:
// Full SSR (default): loader + rendering on serverexport const Route = createFileRoute('/page')({ ssr: true,})// Data-only: loaders run on server, rendering on clientexport const Route = createFileRoute('/heavy-ui')({ ssr: 'data-only',})// Client-only: no SSR at allexport const Route = createFileRoute('/dashboard')({ ssr: false,})
You can even make it dynamic based on route params or search params:
In Next.js, SSR vs static rendering is determined by whether your component uses dynamic APIs (cookies(), headers(), etc.) or dynamic route segments. You don't get the same per-route opt-in/opt-out granularity.
viewport — preloads when the link enters the viewport.
render — preloads as soon as the <Link> mounts.
false — disabled.
When a link is preloaded, the target route's loader runs and results are cached. The loader even receives a cause parameter so you can distinguish preloads from actual navigations.
Next.js automatically prefetches <Link> components that enter the viewport, prefetching the route's loading state (static shell) by default. You can disable it with prefetch={false}, but there's less granularity in choosing the trigger.
Next.js caching depends on the rendering mode: for static rendering, fetch results are cached and served from the Data Cache / Full Route Cache; for dynamic rendering, fetch runs on every request unless you opt into caching. You can control this with fetch options (like cache or next.revalidate) and on-demand invalidation (e.g. revalidatePath).
TanStack Start leans on TanStack Router's loader model, which includes built-in loader caching with configurable staleTime and automatic prefetching.
// TanStack Start: caching is part of the route definitionexport const Route = createFileRoute('/posts')({ loader: () => fetchPosts(), staleTime: 5_000, // data fresh for 5s preloadStaleTime: 30_000, // preloaded data fresh for 30s})
In practice, Next.js "nudges" you toward a cache-aware mental model where you think about static vs dynamic rendering, while Start makes caching feel like part of the router's data-loading contract. If you already like TanStack Query patterns, Start's approach can feel more familiar.
Both frameworks are file-based, but their conventions differ:
Concept
Next.js
TanStack Start
Dynamic params
[slug]/page.tsx
$slug.tsx
Catch-all
[...slug]/page.tsx
$.tsx
Route groups
(group)/page.tsx
(group)/page.tsx
Layouts
layout.tsx (implicit)
Parent route file + <Outlet />
Pathless layouts
(group) convention
_layout.tsx prefix
Error boundary
error.tsx
errorComponent route option
The upside of Start is very strong type inference — route params, search params, and loader data are all typed end-to-end. The cost is a steeper learning curve on naming conventions (__root, $, _, . all have specific meanings).
Next.js is optimized for Vercel but deployable anywhere via standalone output or community adapters.
TanStack Start is built on the Vite Environment API, which means deployment targets are configured through Vite plugins. For example, you can use Nitro's Vite plugin for platforms like Cloudflare Workers, Netlify, or AWS Lambda:
// vite.config.tsimport { tanstackStart } from '@tanstack/react-start/plugin/vite'import { nitro } from 'nitro/vite'export default defineConfig({ plugins: [tanstackStart(), nitro({ preset: 'cloudflare-workers' })],})
The key distinction: Nitro isn't baked into Start — it's one of several deployment adapters you can plug in via Vite. This is a genuine advantage for teams targeting non-Vercel platforms. The deployment story feels more "choose your target" rather than "adapt from the default."
Both approaches work well. Start's version has the advantage of co-locating everything about a route (loader, component, error handler, head) in a single file. Next.js's convention makes it easier to add error handling as an afterthought since it's just dropping a file.
Still in RC — API is stable, but I'd want a 1.0 stamp for long-lived production projects.
No React Server Components yet (planned post-1.0). If RSC is important to your architecture, Next.js is the only option today.
File-based routing rules are strict — though the router watches for file changes and regenerates the route tree automatically, the naming conventions have a learning curve.
Ecosystem and community are smaller. Fewer tutorials, Stack Overflow answers, and production case studies compared to Next.js.
TanStack Start: greenfield apps where type-safe routing and server functions are a priority, you want Vite and multi-platform deployment, and you're comfortable with a pre-1.0 framework. Especially compelling if your team already uses TanStack Router or TanStack Query.
Next.js: teams that need React Server Components, want well-documented caching semantics, progressive-enhanced forms, and a mature framework with a large ecosystem and strong conventions.
TanStack Start and Next.js are solving similar problems with genuinely different philosophies. Start bets on type safety, URL-as-state, and the Vite ecosystem. Next.js bets on Server Components, convention-based architecture, and deep platform integration.
If your team already loves TanStack Router or TanStack Query, Start feels like a natural, cohesive extension. If you want a highly prescriptive full-stack model with RSC, deep caching semantics, and a large body of production patterns, Next.js still feels like the safer default.
The real takeaway for me: these frameworks are making each other better. The competition on type safety, server functions, and developer experience benefits everyone building on React.