Dev Encyclopedia
ArticlesTools

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

ยฉ 2026 Dev Encyclopedia

Privacy PolicyTermsDisclaimer
  1. Home
  2. /Blog
  3. /Migrate Next.js to TanStack Start: Practical Guide with Real Examples
react14 min read

Migrate Next.js to TanStack Start: Practical Guide with Real Examples

How to migrate a Next.js app to TanStack Start: the two-PR strategy, route migration, replacing next/image, and setting up Nitro, with real production lessons from Railway and Inngest.

By Dev EncyclopediaPublished June 21, 2026
On this page

On this page

  • Is This Migration Right for Your App?
  • Setting Up TanStack Start
  • The Two-PR Migration Strategy
  • Migrating Routes
  • Replacing next/image
  • Setting Up Nitro
  • What You Give Up
  • CI/CD Changes
  • Frequently Asked Questions

Railway moved 200+ routes off Next.js onto TanStack Start in two pull requests. Their builds dropped from 10+ minutes to under 2 minutes. Inngest made a similar move shortly after. TanStack Start hit v1.0 in March 2026, and a clear pattern emerged: teams running client-heavy, real-time apps are migrating because Next.js's server-first App Router doesn't add value for their use case.

The common thread is friction. These teams fight the framework more than they benefit from it. Server Components add complexity to dashboards that are 95% client-side. Webpack builds crawl when Vite would finish in seconds. Vercel-specific optimizations don't help when you deploy to Railway, Fly, or bare Docker.

This guide combines official TanStack docs with practical lessons from production migrations. It covers the two-PR strategy Railway used, route conversion patterns, and the trade-offs you should weigh before committing. TanStack Start pairs naturally with a Hono-based API layer, so if your backend is already decoupled, the move is even smoother.

๐Ÿ’ก TL;DR

Decouple from Next.js-specific imports in one PR (next/image, next/router, next/link). Swap the framework in a second PR. This two-step approach keeps both PRs reviewable and each one independently shippable.

Is This Migration Right for Your App?

Good fit for TanStack StartStay on Next.js
App typeClient-heavy SPAs and dashboardsSEO-heavy marketing sites
Real-time needsWebsocket and real-time appsHeavy ISR/ISG usage
Build toolingTeams wanting Vite speedDeep Vercel ecosystem integration
Use caseInternal tools with authReliance on next-seo, next-sitemap ecosystem
ArchitectureApps not using Server ComponentsApps leveraging RSC heavily

Railway's engineering team summarized it well: their app is "overwhelmingly client-side, a rich stateful interface, websockets everywhere." Server Components added nothing. The App Router's file conventions felt like overhead for routes that immediately hydrate into fully interactive client code.

If that description matches your app, keep reading. If your site depends on static generation, edge middleware, or server-side rendering for SEO on every page, Next.js is still the better tool.

Setting Up TanStack Start

For a greenfield project, the scaffolding command gets you running in seconds:

bash โ€” Terminal
npm create @tanstack/start@latest my-app
cd my-app
npm install
npm run dev

For an existing Vite project (or when migrating from Next.js), install the packages manually:

bash โ€” Terminal
npm install @tanstack/react-start @tanstack/react-router vinxi
npm install -D @vitejs/plugin-react vite

Configure Vite with the TanStack Start plugin and Nitro server settings:

typescript โ€” vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { tanstackStart } from "@tanstack/react-start/plugin";

export default defineConfig({
  plugins: [
    tanstackStart({
      react: react(),
      server: {
        preset: "node-server", // or "cloudflare-pages", "vercel", etc.
        routeRules: {
          "/api/**": { cors: true },
        },
      },
    }),
  ],
});
json โ€” package.json (scripts)
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}

The Two-PR Migration Strategy

Railway's approach splits the migration into two independent, reviewable pull requests. Each PR ships on its own. If something breaks, you know exactly which change caused it.

  1. 1

    PR 1: Decouple from Next.js

    Remove every Next.js-specific import and replace it with a framework-agnostic alternative. This PR ships independently and your app continues running on Next.js, just without any proprietary lock-in.

    • next/image โ†’ @unpic/react or a plain <img> with loading="lazy"
    • next/head โ†’ react-helmet-async or direct <meta> tags in your layout
    • next/router (Pages Router) โ†’ a thin wrapper around window.location or a shared navigation abstraction
    • next/link โ†’ plain <a> tags (the prefetching loss is acceptable for most apps)
    • next/dynamic โ†’ React.lazy + Suspense

    โ„น Info

    This is the low-risk PR. Everything still runs on Next.js, builds with Next.js, and deploys the same way. You are only removing imports. Verify nothing breaks, merge it, and move on.

  2. 2

    PR 2: Swap the Framework

    With zero Next.js-specific code remaining, the framework swap becomes mechanical. Convert page files to TanStack Router file-based routes, add the Vite config, and update build scripts.

    1. Convert app/ or pages/ directory structure to TanStack Router's route tree (see the route migration section below)
    2. Add vite.config.ts with the TanStack Start plugin
    3. Replace next.config.js redirects and headers with Nitro routeRules
    4. Update package.json scripts from next dev/next build to vite dev/vite build
    5. Update deployment config (Dockerfile, CI pipeline, hosting platform settings)
    6. Update your CI workflow to use the new build commands

    ๐Ÿ’ก Tip

    Because PR 1 already removed all framework coupling, this PR is purely structural. No business logic changes. If a test fails here, the issue is routing or build config, never application code.

Migrating Routes

TanStack Router uses a $ prefix for dynamic segments instead of Next.js's [bracket] syntax. The file structure maps closely, but data loading and layouts work differently.

Dynamic Routes

Next.js uses bracket notation for dynamic segments. TanStack Router uses a dollar-sign prefix:

typescript โ€” Next.js: app/posts/[slug]/page.tsx
// Next.js App Router
export default async function PostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);
  return <article><h1>{post.title}</h1></article>;
}
typescript โ€” TanStack Start: app/routes/posts/$slug.tsx
// TanStack Start
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/$slug")({
  loader: async ({ params }) => {
    return { post: await getPost(params.slug) };
  },
  component: PostPage,
});

function PostPage() {
  const { post } = Route.useLoaderData();
  return <article><h1>{post.title}</h1></article>;
}

Data Loading

Next.js App Router uses async Server Components or getServerSideProps (Pages Router). TanStack Start uses a loader function on the route definition. The loader runs before the component renders and its return value is available via useLoaderData().

typescript โ€” TanStack Start: loader with error handling
export const Route = createFileRoute("/dashboard")({
  loader: async ({ context }) => {
    const user = await fetchUser(context.auth.userId);
    if (!user) throw redirect({ to: "/login" });
    const [projects, notifications] = await Promise.all([
      fetchProjects(user.id),
      fetchNotifications(user.id),
    ]);
    return { user, projects, notifications };
  },
  component: Dashboard,
});

Nested Layouts

Next.js uses co-located layout.tsx files. TanStack Router uses parent route files that render an <Outlet /> for child content:

typescript โ€” Next.js: app/dashboard/layout.tsx
// Next.js layout
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}
typescript โ€” TanStack Start: app/routes/dashboard.tsx (parent route)
// TanStack Start parent route (acts as layout)
import { createFileRoute, Outlet } from "@tanstack/react-router";

export const Route = createFileRoute("/dashboard")({
  component: DashboardLayout,
});

function DashboardLayout() {
  return (
    <div className="flex">
      <Sidebar />
      <main>
        <Outlet />
      </main>
    </div>
  );
}

โ„น Info

Child routes go in app/routes/dashboard/ (e.g., dashboard/settings.tsx, dashboard/projects.tsx). They automatically render inside the parent's <Outlet />.

Replacing next/image

The next/image component does two things: lazy loading and image optimization (resizing, format conversion, CDN caching). You need to replace both. Two solid options:

Option 1: Edge Image CDN with Plain img Tags

If you deploy behind Cloudflare, Fastly, or any CDN with image transformation, use a plain <img> tag with the CDN's URL pattern. The CDN handles resizing and format negotiation (WebP/AVIF).

tsx โ€” components/OptimizedImage.tsx
interface ImageProps {
  src: string;
  alt: string;
  width: number;
  height: number;
  className?: string;
}

export function OptimizedImage({ src, alt, width, height, className }: ImageProps) {
  // Cloudflare Image Resizing URL format
  const optimizedSrc = `/cdn-cgi/image/width=${width},format=auto/${src}`;

  return (
    <img
      src={optimizedSrc}
      alt={alt}
      width={width}
      height={height}
      loading="lazy"
      decoding="async"
      className={className}
    />
  );
}

Option 2: @unpic/react

@unpic/react is a framework-agnostic image component that generates responsive srcset attributes and handles lazy loading. It works with any image CDN.

bash โ€” Terminal
npm install @unpic/react
tsx โ€” Usage example
import { Image } from "@unpic/react";

export function Hero() {
  return (
    <Image
      src="https://cdn.example.com/hero.jpg"
      alt="Dashboard overview"
      width={1200}
      height={630}
      priority  // Skip lazy loading for above-the-fold images
    />
  );
}

Setting Up Nitro

TanStack Start uses Nitro as its server layer. Nitro handles redirects, security headers, caching rules, and deployment adapters. Railway reported consolidating "500+ redirects, security headers, and caching rules into one place" after the migration.

Here is a typical conversion from Next.js config to Nitro route rules. Nitro's routeRules consolidate caching configuration into a single, declarative format.

javascript โ€” next.config.js (before)
// Next.js redirects and headers
module.exports = {
  async redirects() {
    return [
      { source: "/old-path", destination: "/new-path", permanent: true },
      { source: "/docs/:slug", destination: "/guides/:slug", permanent: true },
    ];
  },
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "X-Frame-Options", value: "DENY" },
          { key: "X-Content-Type-Options", value: "nosniff" },
        ],
      },
      {
        source: "/api/(.*)",
        headers: [
          { key: "Cache-Control", value: "no-store" },
        ],
      },
    ];
  },
};
typescript โ€” vite.config.ts (after, Nitro routeRules)
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { tanstackStart } from "@tanstack/react-start/plugin";

export default defineConfig({
  plugins: [
    tanstackStart({
      react: react(),
      server: {
        preset: "node-server",
        routeRules: {
          // Redirects
          "/old-path": { redirect: { to: "/new-path", statusCode: 301 } },
          "/docs/**": { redirect: { to: "/guides/**", statusCode: 301 } },

          // Security headers (all routes)
          "/**": {
            headers: {
              "X-Frame-Options": "DENY",
              "X-Content-Type-Options": "nosniff",
            },
          },

          // API: no caching
          "/api/**": {
            headers: { "Cache-Control": "no-store" },
            cors: true,
          },

          // Static assets: aggressive caching
          "/assets/**": {
            headers: { "Cache-Control": "public, max-age=31536000, immutable" },
          },
        },
      },
    }),
  ],
});

๐Ÿ’ก Tip

Nitro's deployment presets handle the adapter layer. Switching from Railway (node-server) to Cloudflare Pages is a one-line change: swap the preset string. No other config changes needed.

What You Give Up

Every migration has trade-offs. Be honest about what you lose before committing:

What you loseMitigationImpact
Built-in image optimizationUse @unpic/react or a CDN with image transforms (Cloudflare, Imgix, Cloudinary)Low if you already use a CDN
next-seo and next-sitemap ecosystemWrite meta tags directly or use react-helmet-async. Generate sitemaps with a build script.Medium (one-time setup cost)
Framework maturity (10+ years of Next.js)TanStack Router is mature and battle-tested. TanStack Start (the server layer) is newer, v1.0 since March 2026.Medium for early adopters
Vercel-specific features (Edge Middleware, ISR)Nitro presets cover most hosting platforms. ISR requires a different caching strategy.High if you depend on these features
Larger community and Stack Overflow answersTanStack Discord is active. Docs are thorough. Fewer answers exist for Start-specific issues.Low to medium

CI/CD Changes

Your build commands and output directories change. Update your CI pipeline accordingly.

yaml โ€” .github/workflows/deploy.yml (Next.js, before)
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build  # runs "next build"
      # Output in .next/ directory
yaml โ€” .github/workflows/deploy.yml (TanStack Start, after)
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build  # runs "vite build"
      # Output in .output/ directory
      - name: Start server (or deploy)
        run: node .output/server/index.mjs

โš  Warning

If your hosting platform auto-detects Next.js (Vercel, Netlify, Railway), you may need to update the platform settings to use a generic Node.js buildpack instead. Check your platform's docs for custom build command configuration.

Frequently Asked Questions

Is TanStack Start a replacement for Next.js?

No. It is an alternative for a specific class of applications. TanStack Start excels at client-heavy dashboards, real-time apps, and internal tools where Server Components add complexity without value. Next.js remains the stronger choice for SSR-heavy, SEO-critical marketing sites, content platforms, and teams deeply invested in the Vercel ecosystem.

Think of it as choosing the right tool for the job, not a universal upgrade.

Does TanStack Start support SSR?

Yes. TanStack Start supports SSR, SSG, and client-side rendering. The key difference is philosophy: it does not push you toward server-first patterns by default. You opt into SSR per route when it makes sense, rather than having to opt out of it.

Route loaders can run on the server during SSR or on the client during navigation. You control this at the route level.

How do I migrate Next.js redirects to TanStack Start?

Use Nitro's routeRules in your Vite config. The conversion is mostly mechanical: each source/destination pair becomes a key/value in routeRules.

typescript โ€” vite.config.ts
// Nitro routeRules redirect format
routeRules: {
  "/old-blog/:slug": { redirect: { to: "/articles/:slug", statusCode: 301 } },
  "/legacy-page": { redirect: { to: "/new-page", statusCode: 308 } },
}
What is Nitro in TanStack Start?

Nitro is the server framework layer that powers TanStack Start's backend. It handles redirects, HTTP headers, caching rules, API routes, and deployment adapters. It is the same project that powers Nuxt's server layer, so it is well-tested in production.

  • Deployment presets: one config change switches between Node.js, Cloudflare Workers, Vercel, Deno, and more
  • Route rules: declarative redirects, headers, and caching without middleware code
  • Server routes: file-based API routes with automatic request parsing
  • Auto-imports: utilities like defineEventHandler are available without explicit imports
Can I migrate gradually from Next.js to TanStack Start?

Yes, using the two-PR strategy described in this guide. Decouple from Next.js imports first (PR 1), then swap the framework (PR 2). Each step is independently deployable.

For larger apps, you can also run both frameworks in parallel during the transition. Set up path-based routing at the reverse proxy level (nginx, Cloudflare Workers, or your load balancer) to send some paths to the old Next.js app and others to the new TanStack Start app. Migrate routes in batches until the Next.js instance serves zero traffic, then decommission it.

Related Articles

backend

Hono.js Tutorial: REST API with Zod, JWT & Cloudflare Workers

Step-by-step Hono.js tutorial: routing, middleware, Zod validation, JWT auth, and deployment to Cloudflare Workers and Node.js. Working code throughout.

Jun 15, 2026ยท14 min read
devops

GitHub Actions Tutorial: CI/CD from Push to Deploy (2026)

Learn GitHub Actions: write your first workflow, run tests automatically, use secrets safely, deploy via SSH, cache dependencies, and run matrix builds.

Jun 12, 2026ยท13 min read
devops

Caching Strategies Explained: CDN, Redis & DB Cache

A practical guide to caching strategies: browser cache, CDN, in-process memory, and Redis. Learn which layer to use, cache-aside patterns, and invalidation.

Jun 13, 2026ยท13 min read

On this page

  • Is This Migration Right for Your App?
  • Setting Up TanStack Start
  • The Two-PR Migration Strategy
  • Migrating Routes
  • Replacing next/image
  • Setting Up Nitro
  • What You Give Up
  • CI/CD Changes
  • Frequently Asked Questions
Advertisement