📄 tanstack/start/latest/docs/framework/react/migrate-from-next-js

File: migrate-from-next-js.md | Updated: 11/15/2025

Source: https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js



TanStack

Start v0v0

Search...

+ K

Auto

Log In

TanStack StartRC

Docs Examples GitHub Contributors

TanStack Router

Docs Examples GitHub Contributors

TanStack Query

Docs Examples GitHub Contributors

TanStack Table

Docs Examples Github Contributors

TanStack Formnew

Docs Examples Github Contributors

TanStack DBbeta

Docs Github Contributors

TanStack Virtual

Docs Examples Github Contributors

TanStack Paceralpha

Docs Examples Github Contributors

TanStack Storealpha

Docs Examples Github Contributors

TanStack Devtoolsalpha

Docs Github Contributors

More Libraries

Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide

Documentation

Framework

React logo

React

Version

Latest

Search...

+ K

Menu

Getting Started

Guides

Examples

Tutorials

Framework

React logo

React

Version

Latest

Menu

Getting Started

Guides

Examples

Tutorials

On this page

Migrate from Next.js

Copy Markdown

This guide provides a step-by-step process to migrate a project from the Next.js App Router to TanStack Start. We respect the powerful features of Next.js and aim to make this transition as smooth as possible.

Step-by-Step (Basics)
---------------------

This step-by-step guide provides an overview of how to migrate your Next.js App Router project to TanStack Start. The goal is to help you understand the basic steps involved in the migration process so you can adapt them to your specific project needs.

### Prerequisites

Before we begin, this guide assumes your project structure looks like this:

txt

├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── next.svg
│   ├── vercel.svg
│   └── window.svg
├── README.md
├── src
│   └── app
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx
└── tsconfig.json


├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── next.svg
│   ├── vercel.svg
│   └── window.svg
├── README.md
├── src
│   └── app
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx
└── tsconfig.json

Alternatively, you can follow along by cloning the following starter template :

sh

npx gitpick nrjdalal/awesome-templates/tree/main/next.js-apps/next.js-start next.js-start-er


npx gitpick nrjdalal/awesome-templates/tree/main/next.js-apps/next.js-start next.js-start-er

This structure is a basic Next.js application using the App Router, which we will migrate to TanStack Start.

### 1. Remove Next.js

First, uninstall Next.js and remove related configuration files:

sh

npm uninstall @tailwindcss/postcss next
rm postcss.config.* next.config.*


npm uninstall @tailwindcss/postcss next
rm postcss.config.* next.config.*

### 2. Install Required Dependencies

TanStack Start leverages Vite and TanStack Router:

sh

npm i @tanstack/react-router @tanstack/react-start


npm i @tanstack/react-router @tanstack/react-start

For Tailwind CSS and resolving imports using path aliases:

sh

npm i -D vite @vitejs/plugin-react @tailwindcss/vite tailwindcss vite-tsconfig-paths


npm i -D vite @vitejs/plugin-react @tailwindcss/vite tailwindcss vite-tsconfig-paths

### 3. Update Project Configuration

Now that you've installed the necessary dependencies, update your project configuration files to work with TanStack Start.

  • package.json

json

{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}


{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}
  • vite.config.ts

ts

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

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [\
    tailwindcss(),\
    // Enables Vite to resolve imports using path aliases.\
    tsconfigPaths(),\
    tanstackStart({\
      srcDirectory: 'src', // This is the default\
      router: {\
        // Specifies the directory TanStack Router uses for your routes.\
        routesDirectory: 'app', // Defaults to "routes", relative to srcDirectory\
      },\
    }),\
    viteReact(),\
  ],
})


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

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [\
    tailwindcss(),\
    // Enables Vite to resolve imports using path aliases.\
    tsconfigPaths(),\
    tanstackStart({\
      srcDirectory: 'src', // This is the default\
      router: {\
        // Specifies the directory TanStack Router uses for your routes.\
        routesDirectory: 'app', // Defaults to "routes", relative to srcDirectory\
      },\
    }),\
    viteReact(),\
  ],
})

By default, routesDirectory is set to routes. To maintain consistency with Next.js App Router conventions, you can set it to app instead.

### 4. Adapt the Root Layout

TanStack Start uses a routing approach similar to Remix, with some changes to support nested structures and special features using tokens. Learn more about it at Routing Concepts guide.

Instead of layout.tsx, create a file named __root.tsx in the src/app directory. This file will serve as the root layout for your application.

  • src/app/layout.tsx to src/app/__root.tsx

tsx

- import type { Metadata } from "next"
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router"
import appCss from "./globals.css?url"

- export const metadata: Metadata = { 
-   title: "Create Next App", 
-   description: "Generated by create next app", 
- } 
export const Route = createRootRoute({
  head: () => ({
    meta: [\
      { charSet: "utf-8" },\
      {\
        name: "viewport",\
        content: "width=device-width, initial-scale=1",\
      },\
      { title: "TanStack Start Starter" }\
    ],
    links: [\
      {\
        rel: 'stylesheet',\
        href: appCss,\
      },\
    ],
  }),
  component: RootLayout,
})

- export default function RootLayout({ 
-   children, 
- }: Readonly<{ 
-   children: React.ReactNode
- }>) { 
-   return ( 
-     <html lang="en">
-       <body>{children}</body>
-     </html>
-   ) 
- } 
function RootLayout() {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  )
}


- import type { Metadata } from "next"
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router"
import appCss from "./globals.css?url"

- export const metadata: Metadata = {
-   title: "Create Next App",
-   description: "Generated by create next app",
- }
export const Route = createRootRoute({
  head: () => ({
    meta: [\
      { charSet: "utf-8" },\
      {\
        name: "viewport",\
        content: "width=device-width, initial-scale=1",\
      },\
      { title: "TanStack Start Starter" }\
    ],
    links: [\
      {\
        rel: 'stylesheet',\
        href: appCss,\
      },\
    ],
  }),
  component: RootLayout,
})

- export default function RootLayout({
-   children,
- }: Readonly<{
-   children: React.ReactNode
- }>) { 
-   return ( 
-     <html lang="en">
-       <body>{children}</body>
-     </html>
-   ) 
- }
function RootLayout() {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  )
}

### 5. Adapt the Home Page

Instead of page.tsx, create an index.tsx file for the / route.

  • src/app/page.tsx to src/app/index.tsx

tsx

+ import { createFileRoute } from '@tanstack/react-router'

- export default function Home() { 
+ export const Route = createFileRoute('/')({ 
+   component: Home, 
+ }) 

+ function Home() { 
  return (
    <main className="min-h-dvh w-screen flex items-center justify-center flex-col gap-y-4 p-4">
      <img
        className="max-w-sm w-full"
        src="https://raw.githubusercontent.com/TanStack/tanstack.com/main/public/images/logos/splash-dark.png"
        alt="TanStack Logo"
      />
      <h1>
        <span className="line-through">Next.js</span> TanStack Start
      </h1>
      <a
        className="bg-foreground text-background rounded-full px-4 py-1 hover:opacity-90"
        href="https://tanstack.com/start/latest"
        target="_blank"
      >
        Docs
      </a>
    </main>
  )
}


+ import { createFileRoute } from '@tanstack/react-router'

- export default function Home() { 
+ export const Route = createFileRoute('/')({ 
+   component: Home,
+ }) 

+ function Home() { 
  return (
    <main className="min-h-dvh w-screen flex items-center justify-center flex-col gap-y-4 p-4">
      <img
        className="max-w-sm w-full"
        src="https://raw.githubusercontent.com/TanStack/tanstack.com/main/public/images/logos/splash-dark.png"
        alt="TanStack Logo"
      />
      <h1>
        <span className="line-through">Next.js</span> TanStack Start
      </h1>
      <a
        className="bg-foreground text-background rounded-full px-4 py-1 hover:opacity-90"
        href="https://tanstack.com/start/latest"
        target="_blank"
      >
        Docs
      </a>
    </main>
  )
}

### 6. Are we migrated yet?

Before you can run the development server, you need to create a file that will define the behavior of TanStack Router within TanStack Start.

  • src/router.tsx

tsx

import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  const router = createRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}


import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  const router = createRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

🧠 Here you can configure everything from the default preloading functionality to caching staleness .

Don't worry if you see some TypeScript errors at this point; the next step will resolve them.

### 7. Verify the Migration

Run the development server:

sh

npm run dev


npm run dev

Then, visit http://localhost:3000. You should see the TanStack Start welcome page with its logo and a documentation link.

If you encounter issues, review the steps above and ensure that file names and paths match exactly. For a reference implementation, see the post-migration repository .

Next Steps (Advanced)
---------------------

Now that you have migrated the basic structure of your Next.js application to TanStack Start, you can explore more advanced features and concepts.

### Routing Concepts

| Route Example | Next.js | TanStack Start | | --- | --- | --- | | Root Layout | src/app/layout.tsx | src/app/__root.tsx | | / (Home Page) | src/app/page.tsx | src/app/index.tsx | | /posts (Static Route) | src/app/posts/page.tsx | src/app/posts.tsx | | /posts/[slug] (Dynamic) | src/app/posts/[slug]/page.tsx | src/app/posts/$slug.tsx | | /posts/[...slug] (Catch-All) | src/app/posts/[...slug]/page.tsx | src/app/posts/$.tsx | | /api/endpoint (API Route) | src/app/api/endpoint/route.ts | src/app/api/endpoint.ts |

Learn more about the Routing Concepts .

### Dynamic and Catch-All Routes

Retrieving dynamic route parameters in TanStack Start is straightforward.

tsx

- export default async function Page({ 
-   params, 
- }: { 
-   params: Promise<{ slug: string }> 
- }) { 
+ export const Route = createFileRoute('/app/posts/$slug')({ 
+   component: Page, 
+ }) 

+ function Page() { 
-   const { slug } = await params 
+   const { slug } = Route.useParams() 
  return <div>My Post: {slug}</div>
}


- export default async function Page({
-   params,
- }: {
-   params: Promise<{ slug: string }>
- }) { 
+ export const Route = createFileRoute('/app/posts/$slug')({ 
+   component: Page,
+ }) 

+ function Page() { 
-   const { slug } = await params
+   const { slug } = Route.useParams() 
  return <div>My Post: {slug}</div>
}

Note: If you've made a catch-all route (like src/app/posts/$.tsx), you can access the parameters via const { _splat } = Route.useParams().

Similarly, you can access searchParams using const { page, filter, sort } = Route.useSearch().

Learn more about the Dynamic and Catch-All Routes .

### Links

tsx

- import Link from "next/link"
+ import { Link } from "@tanstack/react-router"

function Component() {
-   return <Link href="/dashboard">Dashboard</Link> 
+   return <Link to="/dashboard">Dashboard</Link> 
}


- import Link from "next/link"
+ import { Link } from "@tanstack/react-router"

function Component() {
-   return <Link href="/dashboard">Dashboard</Link>
+   return <Link to="/dashboard">Dashboard</Link>
}

Learn more about the Links .

### Images

Next.js uses the next/image component for optimized images. In TanStack Start, you can use the package called Unpic for similar functionality and almost a drop-in replacement.

tsx

import Image from 'next/image'
import { Image } from '@unpic/react'
function Component() {
  return (
    <Image
      src="/path/to/image.jpg"
      alt="Description"
      width="600"
      height="400"
      width={600} 
      height={400} 
    />
  )
}


import Image from 'next/image'
import { Image } from '@unpic/react'
function Component() {
  return (
    <Image
      src="/path/to/image.jpg"
      alt="Description"
      width="600"
      height="400"
      width={600}
      height={400}
    />
  )
}

### Server ~Actions~ Functions

tsx

- 'use server'
+ import { createServerFn } from "@tanstack/react-start"

- export const create = async () => { 
+ export const create = createServerFn().handler(async () => { 
  return true
- } 
+ }) 


- 'use server'
+ import { createServerFn } from "@tanstack/react-start"

- export const create = async () => { 
+ export const create = createServerFn().handler(async () => { 
  return true
- } 
+ }) 

Learn more about the Server Functions .

### Server Routes ~Handlers~

ts

- export async function GET() { 
+ export const Route = createFileRoute('/api/hello')({ 
+  server: { 
+     handlers: { 
+       GET: async () => { 
+         return Response.json("Hello, World!")
+       } 
+    } 
+  } 
+ }) 


- export async function GET() { 
+ export const Route = createFileRoute('/api/hello')({ 
+  server: { 
+     handlers: { 
+       GET: async () => { 
+         return Response.json("Hello, World!")
+       } 
+    } 
+  } 
+ }) 

Learn more about the Server Routes .

### Fonts

tsx

- import { Inter } from "next/font/google"

- const inter = Inter({ 
-   subsets: ["latin"], 
-   display: "swap", 
- }) 

- export default function Page() { 
-   return <p className={inter.className}>Font Sans</p> 
- } 


- import { Inter } from "next/font/google"

- const inter = Inter({
-   subsets: ["latin"],
-   display: "swap",
- })

- export default function Page() { 
-   return <p className={inter.className}>Font Sans</p>
- }

Instead of next/font, use Tailwind CSS’s CSS-first approach. Install fonts (for example, from Fontsource ):

sh

npm i -D @fontsource-variable/dm-sans @fontsource-variable/jetbrains-mono


npm i -D @fontsource-variable/dm-sans @fontsource-variable/jetbrains-mono

Add the following to src/app/globals.css:

css

@import 'tailwindcss';

@import '@fontsource-variable/dm-sans'; 
@import '@fontsource-variable/jetbrains-mono'; 

@theme inline {
  --font-sans: 'DM Sans Variable', sans-serif; 
  --font-mono: 'JetBrains Mono Variable', monospace; 
  /* ... */
}

/* ... */


@import 'tailwindcss';

@import '@fontsource-variable/dm-sans';
@import '@fontsource-variable/jetbrains-mono';

@theme inline {
  --font-sans: 'DM Sans Variable', sans-serif; 
  --font-mono: 'JetBrains Mono Variable', monospace; 
  /* ... */
}

/* ... */

### Fetching Data

tsx

- export default async function Page() { 
+ export const Route = createFileRoute('/')({ 
+   component: Page, 
+   loader: async () => { 
+     const res = await fetch('https://api.vercel.app/blog') 
+     return res.json() 
+   }, 
+ }) 

+ function Page() { 
-   const data = await fetch('https://api.vercel.app/blog') 
-   const posts = await data.json() 
+   const posts = Route.useLoaderData() 

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}


- export default async function Page() { 
+ export const Route = createFileRoute('/')({ 
+   component: Page,
+   loader: async () => { 
+     const res = await fetch('https://api.vercel.app/blog') 
+     return res.json() 
+   },
+ }) 

+ function Page() { 
-   const data = await fetch('https://api.vercel.app/blog') 
-   const posts = await data.json() 
+   const posts = Route.useLoaderData() 

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Edit on GitHub

Build from Scratch

Routing

Partners Become a Partner

Code RabbitCode Rabbit CloudflareCloudflare AG GridAG Grid NetlifyNetlify NeonNeon WorkOSWorkOS ClerkClerk ConvexConvex ElectricElectric SentrySentry PrismaPrisma StrapiStrapi UnkeyUnkey

scarf analytics