DEV Community

Victoria
Victoria

Posted on

Beyond Next.js: TanStack Start and the Future of Full-Stack React Development

After 4-5 years of building with Next.js, I've watched the framework evolve from a simple, predictable tool into something far more complex. Next.js remains incredibly powerful for the right use cases. But the constant mental model shifts have become exhausting, and I'm not alone in feeling this way.

When Next.js launched, it was genuinely revolutionary. Before that, building a production-ready React app meant orchestrating webpack, Babel, routing libraries, and countless other tools — each with their own configuration quirks. Next.js said, "Here's one thing. It handles routing, rendering, optimization, everything. Just use it."

The Pages Router was simple and predictable. File-based routing that made intuitive sense. API routes that felt natural. You didn't have to think about the framework — you just built your app. The mental model was consistent.

But at a certain point it went into the questionable direction...

A friendly warning: The article is subjective, expressing my personal feelings, but to make things fair I include other resources so you can form your own opinion. I believe in your independent judgement!

The App Router Era: Power and Complexity

Starting with Next.js 13, things became less stable. React Server Components (RSC) were introduced alongside the App Router, and the framework began changing its foundational assumptions frequently.

Suddenly, everything became "server-side by default." We entered a world of 'use client', 'use server', and the 'use cache' directive. The paradigm flipped entirely, bringing frequent hydration problems.

We adapted to the idea that everything was cached by default in Next.js 14. Then Next.js 15 arrived with Turbopack and a completely inverted mental model: nothing is cached by default. You now have to explicitly opt-in to caching behavior.

// Next.js 15 - Explicit caching with 'use cache' directive
'use cache'

export async function getData() {
  const data = await fetch('/api/data')
  return data
}
Enter fullscreen mode Exit fullscreen mode

Next.js 15 made Turbopack the default (or at least heavily promoted) build tool, moving away from Webpack. The Rust-based bundler promised 10x performance improvements, but real-world data told a more nuanced story. As of 2025, Turbopack is still the direction, but developers report variable experiences — excelling at hot refresh but struggling with broken imports, high resources consumption, and cold starts in some scenarios.

The fact that Vercel published an official guide titled "Ten Common Mistakes with the Next.js App Router" speaks for itself.

The Reality Check

You're probably wondering: 'Does this mean we should ditch Next.js completely?' Absolutely not. Next.js remains excellent at what it does; it shines in specific scenarios but struggles to address what most modern web apps actually need.

Here's the thing: most applications aren't purely server-rendered. Most are a mix. A marketing homepage that needs SEO? Sure, server-render that. But then there's the dashboard, search functionality, user preferences, and interactive features — the stuff that doesn't need (or shouldn't have) server rendering.

With the Next.js App Router, you end up fighting the framework's server-first assumption. You're constantly adding 'use client' boundaries, managing server/client complexity, and dealing with performance trade-offs.

For projects that are truly content-heavy — blogs, documentation sites, e-commerce product catalogs — Next.js still makes total sense. But for the 70% of applications that are interactive with some server-side needs? The friction becomes harder to ignore.

TanStack Start Enters the Arena

That's when TanStack Start enters the picture: a framework built on stable patterns, client-first by design, and refreshingly explicit about what runs where. Here's what makes it different: TanStack has serious credibility. They've been shipping battle-tested tools that developers actually use for years.

  • TanStack Query (formerly React Query) powers data fetching in millions of React applications worldwide
  • TanStack Table powers countless data grids
  • TanStack Router provides type-safe routing for developers who care about type safety

These are battle-tested tools with years of real-world usage, refined through community feedback, and stable APIs that don't flip every version (we can expect TanStack to change, but building blocks remain stable).

When TanStack decided to build a full-stack framework, there was already credibility, an existing philosophy, and a deep understanding of what developers actually need.

This image is quite self-descriptive, I think:
TanStack family

As of November 2025, it's a RC, with active development and growing community adoption. Unlike Next.js, the framework maintains consistency in its fundamentals. Out of curiosity, I built an app while it was still a beta, and now it is v1 already, and everything works without friction.

TansStack page

TanStack Start is built on two key technologies:

  • TanStack Router (the entire routing layer with type safety)
  • Vite (an industry-standard build tool)

This combination matters because each piece is proven, modular, and well-understood.

Core Philosophical Difference: Client-First vs Server-First

Next.js 15: Server-First Architecture

With the App Router, Next.js embraces a server-first paradigm. Every component is a React Server Component by default. You start on the server and explicitly opt into client-side interactivity with 'use client'.

This approach excels for content-heavy websites, blogs, and e-commerce product pages where SEO matters and users primarily consume content.

But for highly interactive applications — dashboards, admin panels, SaaS tools — this creates friction. Developers find themselves constantly fighting the framework's assumptions, marking files with 'use client', and navigating complex server/client boundaries.

TanStack Start: Client-First with Powerful Server Capabilities

TanStack Start takes a different approach: client-first with selective server-side rendering.

Routes are rendered on the server by default for the initial request (providing SSR benefits), but you have fine-grained control over the rendering mode via the ssr property on each route:

// TanStack Start - SSR configuration per route

// Pure client-side rendering (like a traditional SPA)
export const Route = createFileRoute('/dashboard')({
  ssr: false,
  component: DashboardComponent,
})

// Full SSR for SEO-critical pages
export const Route = createFileRoute('/products')({
  ssr: true,
  loader: async () => fetchProducts(),
  component: ProductsComponent,
})

// Data-only SSR: fetch data server-side, render client-side
export const Route = createFileRoute('/admin')({
  ssr: 'data-only',
  loader: async () => fetchAdminData(),
  component: AdminComponent,
})
Enter fullscreen mode Exit fullscreen mode

TanStack features

Key clarification: TanStack Start's "client-first" philosophy means:

  1. The mental model is client-centric: You write code thinking about the client experience first
  2. SSR is opt-in per route: Unlike Next.js where you opt-out of server rendering, TanStack Start lets you opt-in where needed
  3. Code is isomorphic by default: Route loaders run on both server (initial load) and client (navigation)

This gives you the best of both worlds: SSR performance where it matters, with SPA-like navigation for everything else.

Feature-by-Feature Deep Dive

1. Routing with Type Safety

This is where TanStack Start truly shines. The framework generates a routeTree.gen.ts file containing complete type information about every route — a feature Next.js simply doesn't offer.

Next.js 15 Example

// app/products/[slug]/page.tsx
export default async function ProductPage({params}: { params: Promise<{ slug: string }> }) {
  const {slug} = await params
  // Use slug...
  return <div>Product: {slug}</div>
}

// In a component - just strings, no type checking
<Link href={`/products/${productId}`}>
  View Product
</Link>
Enter fullscreen mode Exit fullscreen mode

TanStack Start Example

// routes/products.$id.tsx
export const Route = createFileRoute('/products/$id')({
  loader: async ({params}) => {
    // params.id is fully typed automatically
    return getProduct(params.id)
  },
  component: ProductComponent,
})

function ProductComponent() {
  const product = Route.useLoaderData() // Fully typed!
  return <div>{product.name}</div>
}

// Navigation with compile-time safety
navigate({
  to: '/products/$id',
  params: {id: productId} // TypeScript validates this exists and is correct type
})
Enter fullscreen mode Exit fullscreen mode

Change a route parameter? Every link using that route fails at build time — not at runtime. This eliminates an entire class of bugs before shipping.

Learn more in the TanStack Router Type Safety guide.

2. Data Fetching: Isomorphic Loaders vs Async Server Components

Next.js 15 Approach

// app/page.tsx - Async Server Component
export default async function Page() {
  // Direct data fetching on the server
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()

  return (
    <main>
      <h1>{data.title}</h1>
      <p>{data.description}</p>
    </main>
  )
}

// To cache data in Next.js 15, use 'use cache' directive
'use cache'

export async function getData() {
  const data = await fetch('/api/data')
  return data
}
Enter fullscreen mode Exit fullscreen mode

See Next.js caching documentation.

TanStack Start Approach

export const Route = createFileRoute('/products/$id')({
  loader: async ({params}) => {
    // This loader is ISOMORPHIC:
    // - Runs on server for initial load
    // - Runs on client for subsequent navigation
    const product = await getProduct(params.id)
    const wishlist = await getWishlist()

    return {product, wishlist}
  },
  component: ({useLoaderData}) => {
    const {product, wishlist} = useLoaderData()
    return (
      <div>
        <ProductCard product={product} />
        <WishlistChecker product={product} wishlist={wishlist} />
      </div>
    )
  }
})
Enter fullscreen mode Exit fullscreen mode

These are called "isomorphic loaders" — the same code runs on server during initial load and on client during navigation. This is a fundamental architectural difference.

Here's the key advantage: TanStack Start integrates deeply with TanStack Query. You get automatic caching, stale-while-revalidate, and background refetching out of the box.

Navigate to /products/2, then back to /products/1? The data is still there. No refetch. Instant navigation. It's a cohesive system where data fetching, caching, and navigation work together seamlessly.

Learn about TanStack Start's execution model and isomorphic loaders.

3. Server Functions: Flexibility vs Convention

Next.js 15 Server Actions

// Server Action - tightly coupled to forms
'use server'

export async function createUser(formData: FormData) {
  const name = formData.get('name')

  const newUser = await db.users.create({
    name: name as string
  })

  revalidatePath('/users')
  return newUser
}

// Usage in component
<form action={createUser}>
  <input name="name" />
  <button type="submit">Create</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Server Actions are primarily designed for form submissions and only support POST requests by default. While this provides built-in CSRF protection (comparing Origin and Host headers), it also limits flexibility.

It gets even trickier with middleware, where the exploit helped to bypass a security check.

See Next.js Server Actions documentation and security considerations.

TanStack Start Server Functions

import {createServerFn} from '@tanstack/react-start'
import {z} from 'zod'

export const createUser = createServerFn({method: 'POST'})
  .validator(z.object({
    name: z.string().min(1),
    email: z.email()
  }))
  .middleware([authMiddleware, loggingMiddleware])
  .handler(async ({data, context}) => {
    // data is validated and fully typed
    return db.users.create(data)
  })

// Call it from anywhere - not just forms
const mutation = useMutation({
  mutationFn: createUser
})

<button onClick={() => mutation.mutate({name: 'Alice', email: 'alice@example.com'})}>
  Create User
</button>
Enter fullscreen mode Exit fullscreen mode

TanStack Start server functions support:

  • Any HTTP method (GET, POST, PUT, DELETE, etc.)
  • Built-in validation with Zod or other validators
  • Composable middleware (authentication, logging, etc.)
  • Client-side and server-side middleware execution

While it requires more code, it's far more functional and flexible.

Learn more in the TanStack Start Server Functions guide and Middleware guide.

4. SEO: Static Metadata vs Dynamic Head Management

Both frameworks handle SEO well, but with different approaches.

Next.js 15 Metadata API

import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: Promise<{ id: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // read route params
  const { id } = await params

  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json())

  // optionally access and extend (rather than replace) parent metadata
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

export default function Page({ params, searchParams }: Props) {}
Enter fullscreen mode Exit fullscreen mode

Simple and consistent. Every page exports metadata, and Next.js handles it automatically. The trade-off? Quite verbose, requires additional function for dynamic routes and static in Server Components, less flexible for complex scenarios.

See Next.js Metadata API documentation.

TanStack Start Head Function

export const Route = createFileRoute('/blog/$slug')({
  loader: async ({params}) => {
    const article = await fetchArticle(params.slug)
    return article
  },
  head: ({loaderData}) => ({
    meta: [
      {title: loaderData.title}, // Fully typed from loader!
      {name: 'description', content: loaderData.excerpt},
      {property: 'og:title', content: loaderData.title},
      {property: 'og:description', content: loaderData.excerpt},
      {property: 'og:image', content: loaderData.coverImage},
    ],
    links: [
      {rel: 'canonical', href: `https://example.com/blog/${loaderData.slug}`},
    ],
  }),
  component: BlogPostComponent,
})
Enter fullscreen mode Exit fullscreen mode

The head function receives fully-typed loaderData, ensuring meta tags are never out of sync with your data. Child routes can override parent route meta tags intelligently, creating a composable head management system.

The Real Advantage: Selective SSR for SEO

You choose which routes need server-side rendering:

// Marketing page: Full SSR
export const Route = createFileRoute('/about')({
  ssr: true,
  loader: () => fetchAboutData(),
  component: AboutPage,
})

// Internal dashboard: Pure client-side (no SEO needed)
export const Route = createFileRoute('/dashboard')({
  ssr: false,
  component: Dashboard,
})

// Blog: Static prerendering at build time
export const Route = createFileRoute('/blog/$slug')({
  ssr: 'prerender',
  loader: ({params}) => fetchBlogPost(params.slug),
  component: BlogPost,
})
Enter fullscreen mode Exit fullscreen mode

For applications that are primarily interactive dashboards with some public-facing content, this granular control is invaluable.

TanStack Start even supports static prerendering with intelligent link crawling:

// vite.config.ts
export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: {
        enabled: true,
        autoStaticPathsDiscovery: true,
        crawlLinks: true,
        concurrency: 14,
      },
    }),
  ],
})
Enter fullscreen mode Exit fullscreen mode

The framework automatically crawls your site during build and prerenders all static pages. See the Static Prerendering documentation.

5. The Build Tool Story: Vite vs Turbopack

Next.js 15 Update: Turbopack was introduced as the new build tool (moving away from Webpack), though not yet the absolute default everywhere. Performance improvements are notable but variable depending on project complexity.

Turbopack Performance (Next.js 15 - 2025):

  • Fast Refresh: Improved over Webpack but with variable performance in large monorepos
  • Build speeds: Generally faster for medium projects, but struggles in very large codebases
  • Cold starts: Still an area where some teams report slowness compared to Vite

TanStack Start uses Vite, which has been battle-tested for years across the ecosystem:

  • Predictable performance across different project sizes
  • Mature ecosystem with extensive plugin support
  • No major surprises between versions

I will let you to decide which is better, but in my opinion Turbopack is not as matured as Vite or Webpack, and Vite has stronger positions in comparison with Webpack, so Vite is definitely a winner here.

Learn about Next.js 15 and Turbopack and Vite benchmarks.

6. Deployment: Vendor Lock-in vs True Flexibility

Next.js 15: Optimized for Vercel

Next.js is heavily optimized for Vercel deployment. Deploy to Vercel? Everything works magically.

Self-host? You're fighting against framework assumptions:

  • Build artifacts need environment-specific configuration
  • Image optimization and some performance features tied to Vercel infrastructure
  • Feature parity issues across different hosting providers

My devOps colleague hated it when I used a Next.js middleware in our projects because... Have you ever tried to deploy Next.js apps on AWS? Challenging, to say the least.

Next.js is not build once, run anywhere. You often need to rebuild per environment.

While it's possible to deploy Next.js elsewhere, it requires significantly more configuration and often lacks feature parity with Vercel deployments.

TanStack Start: Deploy Anywhere

Deployment options for TanStack Start

TanStack Start is built on Vite — no vendor lock-in, no environment assumptions.

Deploy to:

Configuration example for Cloudflare:

// vite.config.ts
import {defineConfig} from 'vite'
import {tanstackStart} from '@tanstack/react-start/plugin/vite'
import {cloudflare} from '@cloudflare/vite-plugin'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    cloudflare({viteEnvironment: {name: 'ssr'}}),
    tanstackStart(),
    viteReact(),
  ],
})
Enter fullscreen mode Exit fullscreen mode

The framework doesn't care where you deploy. Build artifacts are truly portable. You build once and run anywhere.

Check this short YouTube video to see how to deploy TanStack Start in less than a minute (it is indeed 1 minute, I tried myself!).

7. Developer Experience: Next.js 15 vs TanStack Start

From my experience, dev experience of Next.js 15 is rather poor. They have announced adequate hydration errors handling just recently. But TanStack Start went far and beyond.

Here is how it looks like starting with TanStack Start:

1) UI option

UI Console config

2) Console option

Tanstack start configuration via console

3) DevTools

TanStack dev tools demo

And what we have with Next.js 15:

1) Console option

Next.js configuration via console

2) DevTools

Next.js 15 dev tools

Well, Next.js can definitely do better?

When to Choose Each Framework

Choose Next.js 15 if:

✅ Building content-heavy sites (blogs, marketing pages, documentation, e-commerce)
✅ SEO is mission-critical with zero-config needs
✅ Deploying to Vercel
✅ Team already knows Next.js thoroughly (sad, but true, learning curve can be a deal breaker if you have juniors in your team)
✅ App is mostly read-heavy with limited interactivity

Choose TanStack Start if:

✅ Building highly interactive applications (dashboards, admin tools, SaaS)
✅ Need deployment flexibility without vendor lock-in (to have some mercy on your devOps engineers)
✅ Type safety across your entire app is non-negotiable
✅ Already using TanStack Query, Table, or Form
✅ Want fine-grained control over SSR per route

You can check the full table here.

The End of the Monopoly

For years, Next.js was the only real choice for full-stack apps. One framework, one pattern, one way to build. While that simplicity helped the ecosystem grow, it also created constraints — not every application fits the server-first mold.

TanStack Start changes that equation. It's not trying to kill Next.js — it's offering developers a genuine alternative with a different philosophy. Client-first, modular, deployment-agnostic, and built on battle-tested libraries.

Next.js isn't going anywhere. It will continue dominating content-heavy sites where its server-first approach makes perfect sense. But TanStack Start brings real competition for interactive applications, and that competition makes the ecosystem healthier.

I've watched it evolve from simple and predictable to powerful but complex. TanStack Start looks promising precisely because it takes a different path — stability over constant reinvention, flexibility over convention, explicit control over implicit defaults.

The React ecosystem needed this. Not because Next.js is bad, but because having genuine alternatives — frameworks competing on merit and philosophy rather than inertia — benefits everyone.

Developers win when they have real choices, not just default options. And right now, TanStack Start is the most compelling alternative I've seen.


Additional Resources

Next.js 15

TanStack Start RC

Deployment Guides

Comparison Resources

Top comments (0)