Why I Switched from Next.js to TanStack Start (And Haven't Looked Back)

By Aziz Ali
tanstack startnextjsboilerplateindie hackersaas
Why I Switched from Next.js to TanStack Start (And Haven't Looked Back)

I almost didn't make the switch. I'd been building on Next.js for three years, and I knew it — deeply. I knew where the bodies were buried, which foot-guns to avoid, which Stack Overflow answers were actually correct. Switching frameworks felt like admitting defeat.

But one Sunday afternoon, after spending four hours debugging a caching bug that I still don't fully understand, I finally snapped. I opened a new terminal, typed npx create-tanstack@latest, and didn't look back. That was six months ago, and every week I'm more convinced it was the right call.

This is the honest story of why I switched from Next.js to TanStack Start — the real frustrations that pushed me out, what surprised me when I arrived, and why I built BetterStarter entirely on TanStack Start.


The Next.js Pain That Built Up Over Time

Let me be clear: Next.js isn't bad. It's an impressive piece of engineering. The Vercel team is legitimately talented, and version 13+ brought real innovations. But somewhere along the way, building with Next.js started feeling like fighting the framework instead of building my product.

The App Router Mental Model Is Genuinely Hard

I've been a professional developer for over a decade. I'm comfortable with complex systems. But the App Router's mental model for server components broke my brain in ways I didn't expect.

The core problem: you're writing code that looks like React but doesn't behave like React. Components can be async on the server but not on the client. You can await things at the component level — which looks incredible in demos but creates subtle, hard-to-debug issues in real apps. The rules around what can be in a Server Component vs. a Client Component feel arbitrary until you internalize a whole new mental model, and even then, you'll violate them by accident constantly.

I once spent an afternoon tracking down a bug where a third-party library worked fine in Pages Router apps but silently broke in App Router because it used browser APIs at module level. There was no error. Just wrong behavior.

The Pages Router had a clear mental model: everything is client-side React, getServerSideProps runs on the server, getStaticProps runs at build time. Simple. App Router replaced that with a spectrum of component types, caching behaviors, and rendering strategies that interact in non-obvious ways.

Unpredictable Caching Is a Feature, Apparently

I want to tell you about the caching system.

In Next.js App Router, there are four separate caching layers:

  1. Request Memoization
  2. Data Cache
  3. Full Route Cache
  4. Router Cache

Each has different invalidation rules. Each interacts with the others. fetch() inside a component gets memoized automatically. But only sometimes. Unless you pass { cache: 'no-store' }. Or call revalidatePath(). Or revalidateTag(). Or wait for the TTL to expire.

I shipped a bug to production where a user's dashboard was showing stale data because the Router Cache held onto a cached response for 30 seconds by default — behavior I didn't know existed. The user thought the app was broken. They were right.

The Vercel docs have a whole page trying to explain this. The existence of that page is the problem. Caching should be explicit and predictable, not something you need a diagram to understand.

Vercel Lock-In Is Real

I have nothing against Vercel as a product. Their DX is excellent. But Next.js is built first and foremost for Vercel, and that matters when you're building a product that might need to run elsewhere.

Edge Functions behave differently on Vercel than on Cloudflare Workers. The Image Optimization API requires Vercel's runtime. Middleware has size limits that only apply to certain environments. ISR (Incremental Static Regeneration) works correctly on Vercel and is a pain everywhere else.

When I started working on BetterStarter, I wanted the boilerplate to be deployable anywhere — Fly.io, Railway, a plain VPS — without forcing people into a specific vendor. With Next.js, that felt like swimming upstream.

The Config Files Multiplied Like Rabbits

A fresh Next.js project in 2025 comes with:

  • next.config.js (or .ts or .mjs, pick your poison)
  • middleware.ts
  • tailwind.config.ts
  • postcss.config.js
  • tsconfig.json
  • .eslintrc.json
  • app/layout.tsx
  • app/page.tsx

And before you've written a line of actual product code, you're configuring how app/layout.tsx wraps app/(auth)/layout.tsx which wraps app/(auth)/login/page.tsx. The file-system routing is powerful but it encourages a deeply nested file structure that becomes hard to navigate in any real app.


The Moment I Discovered TanStack Start

I'd been following the TanStack ecosystem for years — TanStack Query is one of the best libraries ever written, and I'd used TanStack Table and TanStack Router in side projects. When Tanner Linsley announced TanStack Start, I was curious but skeptical. "Another full-stack framework" was my first reaction.

Then I watched the introductory demo and noticed something: the data flow was obvious. There was no magic. No implicit server/client boundary you had to infer. The createServerFn API made server-side logic explicit and composable. TanStack Router's type-safety was end-to-end — route params, search params, loader data — all typed at the framework level, not bolted on with third-party tools.

I built a small project with it over a weekend. By Sunday night, I was re-reading my own code and thinking "this is just... clear." The routing was explicit. The data loading was explicit. The server/client boundary was explicit. Everything that Next.js App Router made implicit and magical, TanStack Start made visible and controllable.


What Actually Surprised Me

Server Functions Are First-Class Citizens

The createServerFn API is genuinely elegant:

import { createServerFn } from '@tanstack/start'
import { db } from '~/db'

export const getUser = createServerFn({ method: 'GET' })
  .validator((data: { userId: string }) => data)
  .handler(async ({ data }) => {
    const user = await db.query.users.findFirst({
      where: (users, { eq }) => eq(users.id, data.userId),
    })
    return user
  })

// In a route loader:
export const Route = createFileRoute('/dashboard')({
  loader: ({ context }) => getUser({ data: { userId: context.auth.userId } }),
})

This is a server function. It's explicitly typed. It validates its input. It runs on the server. And it's called like a normal function from a route loader — no "use server" directive, no magic string, no guessing about where it runs.

Compare that to Next.js Server Actions, where the boundary is a string directive and the mental model requires knowing which components can call actions and which can't.

TanStack Router's Type Safety Is Unreal

I've used typed routers before. I've used next-intl with type-safe routes, React Router with generics, all of it. Nothing compares to TanStack Router's end-to-end type safety.

Route params are typed at the router level. Search params are validated with a schema and returned typed. Loader data is available in components with full type inference — no useLoaderData<typeof loader> casting required. If you change a route's params, TypeScript tells you every place in your codebase that needs updating.

For a production SaaS, this is not a nice-to-have. It's the difference between refactoring with confidence and refactoring while crossing your fingers.

File-Based Routing That Makes Sense

TanStack Start uses TanStack Router's file-based routing, which is flat by design. Instead of deeply nested app/(auth)/(dashboard)/settings/profile/page.tsx, you get:

routes/
  __root.tsx
  _auth.tsx
  _auth.dashboard.tsx
  _auth.dashboard.settings.tsx
  index.tsx

Layout routes are prefixed with _. Route groups are obvious. The whole routing tree is visible in a single directory rather than scattered across nested folders. I can open the routes/ directory and immediately understand the app's structure.


The Migration Pain (Being Honest)

I'm not going to pretend the switch was painless. It wasn't.

The ecosystem is smaller. Next.js has years of "how to do X with Next.js" content. TanStack Start is newer. I hit edge cases where I had to read source code or ask in the Discord because there was no blog post covering my specific problem.

Some libraries need client-side workarounds. A couple of npm packages assumed a Next.js or Vite environment in ways that required small shims. Nothing insurmountable, but it cost time.

The mental model takes adjustment. TanStack Start's loader pattern means data fetching happens in route loaders, not inside components. If you're used to sprinkling useQuery calls throughout a component tree, you'll need to restructure your thinking. After about two weeks, it clicked — and now I think it's better — but the learning curve is real.


Why I Built BetterStarter on TanStack Start

When I decided to build a SaaS boilerplate for indie hackers, I had a choice: build on the mainstream option (Next.js) or bet on the framework I believed in (TanStack Start).

I chose TanStack Start. Here's why:

The indie hacker audience ships fast and iterates often. You need a framework where the codebase stays understandable as it grows, where you can debug data issues without reading cache invalidation docs, and where you're not tied to a specific hosting provider.

BetterStarter ships with TanStack Start as the foundation, plus everything a SaaS needs on day one:

  • Auth — email/password + OAuth, fully configured with session management
  • Stripe — checkout, webhooks, subscription management, all wired up
  • Email — transactional emails via Resend, with templates
  • Drizzle ORM — type-safe database queries with migrations
  • shadcn/ui + Tailwind — a real UI system, not bare components
  • TypeScript throughout — no any-casting escape hatches

The TanStack Start foundation means the architecture is clear from the start. Loader data flows top-down. Server functions are explicit. Route params are typed. When you add a feature at 2am, the framework isn't going to surprise you.

You can read more about the technical decisions in The Best TanStack Start Boilerplate in 2025 and how it stacks up against alternatives in TanStack Start vs Next.js: An Honest Comparison for 2025.


The Verdict: Would I Switch Again?

Every few months I check what's changed in Next.js. I read the release notes, try the new features, watch the talks. Each time I come back to TanStack Start more convinced.

The problems I had with Next.js — the caching complexity, the implicit server/client boundary, the Vercel dependency — haven't been solved. They've been iterated on and documented better, which is appreciated, but the underlying architecture remains the same.

TanStack Start is not perfect. The ecosystem is still maturing. There will be moments where you're on your own. But the foundation is clear, the type safety is exceptional, and the team behind it (the same team that made TanStack Query, the most-used async state library in React) knows what they're doing.

If you're starting a new SaaS project in 2026, I'd choose TanStack Start. And if you want to start with all the plumbing already done — auth, Stripe, email, database — BetterStarter is $99 one-time, no subscription, no vendor lock-in.


FAQ

Why switch from Next.js to TanStack Start?

The main reasons are clarity and control. TanStack Start makes server/client boundaries explicit, has end-to-end TypeScript type safety in routing and data loading, and doesn't impose platform lock-in. Developers frustrated with Next.js App Router's caching complexity and RSC mental model often find TanStack Start more predictable and easier to debug.

Is TanStack Start production-ready in 2025?

Yes. TanStack Start reached stable release and is used in production by multiple teams. The core team is well-established — they maintain TanStack Query, one of the most widely used React libraries. The ecosystem is smaller than Next.js but mature enough for serious projects.

How hard is it to migrate from Next.js to TanStack Start?

Migration requires restructuring data fetching (moving from component-level useQuery calls or Server Components to route loaders), converting API routes to server functions, and adapting file-based routing conventions. For a medium-sized app, plan for 1–3 weeks. Starting a new project with TanStack Start from scratch is much smoother.

What are the main differences between Next.js App Router and TanStack Start?

TanStack Start uses explicit createServerFn for server logic instead of implicit Server Components, has file-based routing via TanStack Router (flat rather than nested), and provides end-to-end type-safe route params and loader data. Next.js has a larger ecosystem and stronger Vercel integration. TanStack Start is more explicit and deployable anywhere.

Does TanStack Start work with Drizzle ORM and Stripe?

Yes — and this combination works well. Drizzle integrates cleanly with TanStack Start's server functions, and Stripe's Node.js SDK works in server functions without any special configuration. The BetterStarter boilerplate ships with both pre-configured so you can skip the wiring entirely.