{"id":392,"date":"2026-06-28T05:03:15","date_gmt":"2026-06-28T05:03:15","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/tanstack-start-vs-nextjs-where-i-switched-and-where-i-stayed\/"},"modified":"2026-06-28T05:03:15","modified_gmt":"2026-06-28T05:03:15","slug":"tanstack-start-vs-nextjs-where-i-switched-and-where-i-stayed","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/tanstack-start-vs-nextjs-where-i-switched-and-where-i-stayed\/","title":{"rendered":"TanStack Start vs Next.js: Where I Switched and Where I Stayed"},"content":{"rendered":"<p>Okay, confession. I started a side project in February on Next.js, hit a wall around the caching defaults, and instead of grinding through it I rage-quit and rebuilt the whole thing on <a href=\"https:\/\/tanstack.com\/start\/latest\" rel=\"nofollow noopener\" target=\"_blank\">TanStack Start<\/a>. Three months later the app is in production, I&rsquo;ve shipped two more features on top of it, and I have actual opinions about where the trade is worth it.<\/p>\n<p>This is not the &ldquo;TanStack Start killed Next.js&rdquo; post. Next.js still ships most of my client work. But there&rsquo;s a real chunk of apps where Start fits my brain better, and I want to walk through where the line is for me before I forget what convinced me.<\/p>\n<p>Short version for the impatient: if your app is mostly authenticated, data-heavy, and you don&rsquo;t need RSC streaming for SEO, give Start a serious look. If you&rsquo;re building a marketing site, a content blog, or anything that depends on RSC payload streaming as a SEO feature, stay on Next.<\/p>\n<h2 id=\"tanstack-start-is-a-router-first-framework-not-a-nextjs-clone\">TanStack Start is a router-first framework, not a Next.js clone<\/h2>\n<p>The first thing that confused me is that Start isn&rsquo;t really competing on the same axis as Next.js. Next.js sells you a framework that happens to have a router. Start sells you a router that happens to be a framework. That sounds like a marketing distinction, but it changes how you build.<\/p>\n<p>In Next.js, your file paths drive everything: a folder is a route, <code>page.tsx<\/code> is the entry, <code>layout.tsx<\/code> wraps it, and the framework decides which boundaries are server and which are client. In Start, you define routes as objects with a <code>loader<\/code>, a <code>component<\/code>, and optional <code>beforeLoad<\/code>. The whole tree is one typed graph, and your loaders&rsquo; return types flow into your components without a <code>useQuery<\/code> round trip.<\/p>\n<p>Here&rsquo;s the same &ldquo;load user, render page&rdquo; in both. Next.js 15 with the app router:<\/p>\n<pre><code class=\"language-tsx\">\/\/ app\/dashboard\/page.tsx\nimport { getUserFromCookie } from &quot;@\/lib\/auth&quot;;\n\nexport default async function DashboardPage() {\n  const user = await getUserFromCookie();\n  return &lt;h1&gt;Hi {user.name}&lt;\/h1&gt;;\n}\n<\/code><\/pre>\n<p>TanStack Start:<\/p>\n<pre><code class=\"language-tsx\">\/\/ src\/routes\/dashboard.tsx\nimport { createFileRoute } from &quot;@tanstack\/react-router&quot;;\nimport { getUserFromCookie } from &quot;~\/lib\/auth&quot;;\n\nexport const Route = createFileRoute(&quot;\/dashboard&quot;)({\n  loader: () =&gt; getUserFromCookie(),\n  component: Dashboard,\n});\n\nfunction Dashboard() {\n  const user = Route.useLoaderData();\n  return &lt;h1&gt;Hi {user.name}&lt;\/h1&gt;;\n}\n<\/code><\/pre>\n<p>Both work. The Next.js one is shorter on the page. The Start one gives you the loader as a first-class hook you can refetch, invalidate, or subscribe to from any child. Six months in, the loader pattern is the thing I miss the most when I jump back into Next.js for client projects.<\/p>\n<h2 id=\"where-i-migrated-and-what-i-kept-on-nextjs\">Where I migrated and what I kept on Next.js<\/h2>\n<p>The migration math is the boring part of this post, but I think it matters. My side project is a small internal tool: roughly 40 routes, mostly authenticated, lots of forms, no public SEO surface. Migrating took about a week of evenings. About 70% of that was rewriting <code>app\/<\/code> route folders into Start&rsquo;s <code>src\/routes\/<\/code> file structure and porting server actions to server functions. The rest was rewiring auth.<\/p>\n<p>I did not migrate my actual client portfolio, which still runs on Next.js. The marketing pages there depend on <code>generateStaticParams<\/code> and RSC streaming for first paint on slow networks, and Start&rsquo;s SSR story doesn&rsquo;t ship the same RSC streaming yet. If you&rsquo;ve ever fought with <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-app-router-caching-defaults-that-burned-me\/\" rel=\"noopener\">Next.js caching defaults<\/a> and lived to tell about it, you know the price of leaving that ecosystem isn&rsquo;t zero. The plugin and host story is just deeper.<\/p>\n<p>So the rule I ended up with is: data app, internal tool, anything where I&rsquo;m the only audience for the SSR markup, Start. Public-facing content site, marketing landing pages, anywhere I&rsquo;m going to brag about Core Web Vitals, Next.<\/p>\n<h2 id=\"server-functions-are-the-part-i-actually-missed\">Server functions are the part I actually missed<\/h2>\n<p>This is the feature I didn&rsquo;t know I wanted until I had it. In Next.js, you write a server action and call it from a form. Fine. The action lives in a <code>\"use server\"<\/code> file and you can also call it imperatively, but the type story gets weird the moment you want to call it from a loader or from the client outside a form.<\/p>\n<p>Start&rsquo;s server functions feel more like RPC done right. You define them once, they run on the server, and you can call them from a loader or from a button handler on the client. The types travel with the function, so I haven&rsquo;t written a <code>fetch<\/code> wrapper in months.<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/server\/posts.ts\nimport { createServerFn } from &quot;@tanstack\/start&quot;;\nimport { z } from &quot;zod&quot;;\nimport { db } from &quot;~\/lib\/db&quot;;\n\nexport const getPosts = createServerFn({ method: &quot;GET&quot; })\n  .validator(z.object({ limit: z.number() }))\n  .handler(async ({ data }) =&gt; {\n    return db.post.findMany({ take: data.limit });\n  });\n<\/code><\/pre>\n<p>Then in a loader:<\/p>\n<pre><code class=\"language-ts\">loader: () =&gt; getPosts({ data: { limit: 20 } }),\n<\/code><\/pre>\n<p>Or from a button click on the client, no extra boilerplate:<\/p>\n<pre><code class=\"language-tsx\">const onClick = async () =&gt; {\n  const fresh = await getPosts({ data: { limit: 50 } });\n  setPosts(fresh);\n};\n<\/code><\/pre>\n<p>One definition. Validation and the return type live next to the handler. This is the part where I stopped writing custom <code>\/api\/posts\/route.ts<\/code> files and just deleted them. I cover the same pattern from a different angle in my notes on <a href=\"https:\/\/abrarqasim.com\/blog\/trpc-in-2026-the-type-safe-api-i-actually-reach-for\/\" rel=\"noopener\">tRPC in 2026<\/a>, and the punchline is similar: when the boundary between client and server is a typed function, your brain stops context-switching between two languages.<\/p>\n<h2 id=\"middleware-that-runs-in-the-same-type-safe-world\">Middleware that runs in the same type-safe world<\/h2>\n<p>Next.js middleware lives in <code>middleware.ts<\/code> at the project root, runs at the edge, and gets a <code>NextRequest<\/code>. It&rsquo;s powerful, but it&rsquo;s also a context all by itself: you can&rsquo;t easily share state with your route handlers, and the type story is, let&rsquo;s be polite, optimistic.<\/p>\n<p>Start&rsquo;s middleware composes with <code>createMiddleware<\/code> and runs through the same server-function pipeline. You can stack auth checks, rate limits, and logging, and each step can attach typed context that the next step receives. Here&rsquo;s an auth middleware I actually use:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/server\/middleware.ts\nimport { createMiddleware } from &quot;@tanstack\/start&quot;;\nimport { getUserFromCookie } from &quot;~\/lib\/auth&quot;;\n\nexport const authMiddleware = createMiddleware()\n  .server(async ({ next }) =&gt; {\n    const user = await getUserFromCookie();\n    if (!user) throw new Error(&quot;unauthorized&quot;);\n    return next({ context: { user } });\n  });\n<\/code><\/pre>\n<p>Then I attach it to a server function:<\/p>\n<pre><code class=\"language-ts\">export const deletePost = createServerFn({ method: &quot;POST&quot; })\n  .middleware([authMiddleware])\n  .validator(z.object({ id: z.string() }))\n  .handler(async ({ data, context }) =&gt; {\n    if (context.user.role !== &quot;admin&quot;) throw new Error(&quot;forbidden&quot;);\n    return db.post.delete({ where: { id: data.id } });\n  });\n<\/code><\/pre>\n<p>The <code>context.user<\/code> is typed because the middleware said so. No re-fetching, no parallel auth call in the handler, no <code>getServerSession<\/code> boilerplate. This is the kind of small ergonomic win that compounds across forty routes.<\/p>\n<p>If you want to read the actual spec, the <a href=\"https:\/\/tanstack.com\/router\/latest\/docs\/framework\/react\/guide\/route-context\" rel=\"nofollow noopener\" target=\"_blank\">TanStack Router middleware docs<\/a> cover the full type-context flow, and the <a href=\"https:\/\/tanstack.com\/start\/latest\/docs\/framework\/react\/server-functions\" rel=\"nofollow noopener\" target=\"_blank\">Start server functions reference<\/a> is the canonical source for how the validators and middleware chain together.<\/p>\n<h2 id=\"what-id-still-pick-nextjs-for\">What I&rsquo;d still pick Next.js for<\/h2>\n<p>I want to push back on the &ldquo;Start is just better&rdquo; framing I keep seeing on Twitter, because it isn&rsquo;t true for me. There are real reasons I still ship Next.js for client work, and I think it&rsquo;s honest to list them.<\/p>\n<p>First, RSC streaming. Next.js 15 streams the React Server Component payload over the wire and progressively hydrates, which is genuinely useful for public-facing pages where TTFB matters. Start does SSR, but its RSC story is younger and the streaming defaults are less aggressive. For an e-commerce product page, I&rsquo;d still grab Next.<\/p>\n<p>Second, the hosting story. Vercel is opinionated and expensive, but it&rsquo;s also one CLI command to deploy and the cache layer Just Works. Start runs anywhere a Vite app runs, which is great in theory and a small project in practice. I deployed mine to Cloudflare Workers and it works, but I had to read the Cloudflare adapter docs to wire it up.<\/p>\n<p>Third, the ecosystem. Next.js has a decade of Stack Overflow answers, every auth provider has a Next.js example in the README, and every &ldquo;How do I do X in React?&rdquo; tutorial assumes Next. Start is much smaller. When I hit a weird SSR cookie bug, I had to read source code, not blog posts. That&rsquo;s fine for me, less fine for a team rotating in junior devs.<\/p>\n<p>I&rsquo;m not selling Start as a Next.js replacement. I&rsquo;m saying it&rsquo;s a better fit for a specific shape of app, and pretending otherwise would be cope.<\/p>\n<h2 id=\"how-to-try-it-without-rewriting-your-whole-app\">How to try it without rewriting your whole app<\/h2>\n<p>If you want to test the waters without burning a weekend, I&rsquo;d do this. Pick one internal tool you maintain, ideally something with a login wall and a couple of CRUD pages. Spin up a new Start project with <code>npx create-tsrouter-app@latest<\/code>, port two routes by hand, and write one server function. That&rsquo;s enough to feel whether the loader-and-server-function pattern clicks for you.<\/p>\n<p>If it does, I&rsquo;d port the rest. If it doesn&rsquo;t, you&rsquo;ve spent two hours and learned what your real attachment to file-based RSC routing is, which is useful information either way.<\/p>\n<p>I keep a running list of small framework experiments like this on my <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">portfolio<\/a>, and the pattern is always the same: pick a real app, give it a week, and write down the moment you wanted to throw the laptop. That moment is where the trade-off actually lives. Mine for Start was when I tried to deploy to a non-Vercel host. Yours might be different. Worth finding out before you bet a client project on it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I rebuilt a side project on TanStack Start after rage-quitting Next.js. Here&#8217;s where Start fits my brain better, and where Next.js still wins for me.<\/p>\n","protected":false},"author":2,"featured_media":391,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I rebuilt a side project on TanStack Start after rage-quitting Next.js. Here's where Start fits my brain better, and where Next.js still wins for me.","rank_math_focus_keyword":"tanstack start","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[354,35],"tags":[61,41,423,422,421,420,200],"class_list":["post-392","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react","category-web-development","tag-nextjs","tag-react","tag-react-server-components","tag-ssr","tag-tanstack-router","tag-tanstack-start","tag-vite"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/392","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=392"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/392\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/391"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=392"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=392"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=392"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}