Skip to main content

migrate-from-nextjs

Step-by-step migration from Next.js App Router to TanStack Start: route definition conversion, API mapping, server function conversion from Server Actions, middleware conversion, data fetching pattern changes.

Informations de source

Dépôt
TanStack/router
Dernière activité de la source
26 juillet 2026 à 16:53
Langue détectée de SKILL.md
anglais
Étoiles
15 202
Forks
1 900

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
migrate-from-nextjs
description
Step-by-step migration from Next.js App Router to TanStack Start: route definition conversion, API mapping, server function conversion from Server Actions, middleware conversion, data fetching pattern changes.
metadata
{"type":"lifecycle","library":"tanstack-start","library_version":"1.168.32"}
requires
["start-core","react-start"]
sources
["TanStack/router:docs/start/framework/react/guide/server-functions.md","TanStack/router:docs/start/framework/react/guide/middleware.md","TanStack/router:docs/start/framework/react/guide/execution-model.md"]
# Migrate from Next.js App Router to TanStack Start This is a step-by-step migration checklist. Complete tasks in order. > **CRITICAL**: TanStack Start is isomorphic by default. ALL code runs in both environments unless you use `createServerFn`. This is the opposite of Next.js Server Components, where code is server-only by default. > **CRITICAL**: TanStack Start uses `createServerFn`, NOT `"use server"` directives. Do not carry over any `"use server"` or `"use client"` directives. > **CRITICAL**: Types are FULLY INFERRED in TanStack Router/Start. Never cast, never annotate inferred values. ## Pre-Migration - [ ] **Create a migration branch** ```bash git checkout -b migrate-to-tanstack-start ``` - [ ] **Install TanStack Start** ```bash npm i @tanstack/react-start @tanstack/react-router npm i -D vite @vitejs/plugin-react ``` - [ ] **Remove Next.js** ```bash npm uninstall next @next/font @next/image ``` ## Concept Mapping | Next.js App Router | TanStack Start | | -------------------------------- | ------------------------------------------------------------------------- | | `app/page.tsx` | `src/routes/index.tsx` | | `app/layout.tsx` | `src/routes/__root.tsx` | | `app/posts/[id]/page.tsx` | `src/routes/posts/$postId.tsx` | | `app/api/users/route.ts` | `src/routes/api/users.ts` (server property) | | `"use server"` + Server Actions | `createServerFn()` | | `"use client"` | Not needed (everything is isomorphic) | | Server Components (default) | All components are isomorphic; use `createServerFn` for server-only logic | | `next/navigation` `useRouter` | `useRouter()` from `@tanstack/react-router` | | `next/link` `Link` | `<Link>` from `@tanstack/react-router` | | `next/head` or `metadata` export | `head` property on route | | `middleware.ts` (edge) | `createMiddleware()` in `src/start.ts` | | `next.config.js` | `vite.config.ts` with `tanstackStart()` | | `generateStaticParams` | `prerender` config in `vite.config.ts` | ## Step 1: Vite Configuration Replace `next.config.js` with: ```ts // vite.config.ts import { defineConfig } from 'vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite' import viteReact from '@vitejs/plugin-react' export default defineConfig({ plugins: [ tanstackStart(), // MUST come before react() viteReact(), ], }) ``` Update `package.json`: ```json { "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "start": "node .output/server/index.mjs" } } ``` ## Step 2: Router Factory ```tsx // src/router.tsx import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' export function getRouter() { const router = createRouter({ routeTree, scrollRestoration: true, }) return router } ``` ## Step 3: Convert Layout → Root Route Next.js: ```tsx // app/layout.tsx export const metadata = { title: 'My App' } export default function RootLayout({ children }) { return ( <html> <body>{children}</body> </html> ) } ``` TanStack Start: ```tsx // src/routes/__root.tsx import type { ReactNode } from 'react' import { Outlet, createRootRoute, HeadContent, Scripts, } from '@tanstack/react-router' export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { title: 'My App' }, ], }), component: RootComponent, }) function RootComponent() { return ( <html> <head> <HeadContent /> </head> <body> <Outlet /> <Scripts /> </body> </html> ) } ``` ## Step 4: Convert Pages → File Routes Next.js: ```tsx // app/posts/[id]/page.tsx export default function PostPage({ params }: { params: { id: string } }) { // ... } ``` TanStack Start: ```tsx // src/routes/posts/$postId.tsx import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/posts/$postId')({ component: PostPage, }) function PostPage() { const { postId } = Route.useParams() // ... } ``` Key differences: - Dynamic segments use `$param` not `[param]` - Params accessed via `Route.useParams()` not component props - Route path in filename uses `.` or `/` separators ## Step 5: Convert Server Actions → Server Functions Next.js: ```tsx // app/actions.ts 'use server' export async function createPost(formData: FormData) { const title = formData.get('title') as string await db.posts.create({ title }) } ``` TanStack Start: ```tsx // src/utils/posts.functions.ts import { createServerFn } from '@tanstack/react-start' export const createPost = createServerFn({ method: 'POST' }) .validator((data) => { if (!(data instanceof FormData)) throw new Error('Expected FormData') return { title: data.get('title')?.toString() || '' } }) .handler(async ({ data }) => { await db.posts.create({ title: data.title }) return { success: true } }) ``` ## Step 6: Convert Data Fetching Next.js Server Component: ```tsx // app/posts/page.tsx (Server Component — server-only by default) export default async function PostsPage() { const posts = await db.posts.findMany() return <PostList posts={posts} /> } ``` TanStack Start: ```tsx // src/routes/posts.tsx import { createFileRoute } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start' const getPosts = createServerFn({ method: 'GET' }).handler(async () => { return db.posts.findMany() }) export const Route = createFileRoute('/posts')({ loader: () => getPosts(), // loader is isomorphic, getPosts runs on server component: PostsPage, }) function PostsPage() { const posts = Route.useLoaderData() return <PostList posts={posts} /> } ``` ## Step 7: Convert API Routes → Server Routes Next.js: ```ts // app/api/users/route.ts export async function GET() { const users = await db.users.findMany() return Response.json(users) } ``` TanStack Start: ```ts // src/routes/api/users.ts import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/api/users')({ server: { handlers: { GET: async () => { const users = await db.users.findMany() return Response.json(users) }, }, }, }) ``` ## Step 8: Convert Navigation Next.js: ```tsx import Link from 'next/link' ;<Link href={`/posts/${post.id}`}>View Post</Link> ``` TanStack Start: ```tsx import { Link } from '@tanstack/react-router' ;<Link to="/posts/$postId" params={{ postId: post.id }}> View Post </Link> ``` Never interpolate params into the `to` string. Use `params` prop. ## Step 9: Convert Middleware Next.js: ```ts // middleware.ts export function middleware(request: NextRequest) { const token = request.cookies.get('session') if (!token) return NextResponse.redirect(new URL('/login', request.url)) } export const config = { matcher: ['/dashboard/:path*'] } ``` TanStack Start: ```tsx // src/start.ts — must be manually created import { createStart, createMiddleware } from '@tanstack/react-start' import { redirect } from '@tanstack/react-router' const authMiddleware = createMiddleware().server(async ({ next, request }) => { const cookie = request.headers.get('cookie') if (!cookie?.includes('session=')) { throw redirect({ to: '/login' }) } return next() }) export const startInstance = createStart(() => ({ requestMiddleware: [authMiddleware], })) ``` ## Step 10: Convert Metadata/SEO Next.js: ```tsx export const metadata = { title: 'Post Title', description: 'Post description', } ``` TanStack Start: ```tsx export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => fetchPost(params.postId), head: ({ loaderData }) => ({ meta: [ { title: loaderData.title }, { name: 'description', content: loaderData.excerpt }, { property: 'og:title', content: loaderData.title }, ], }), }) ``` ## Post-Migration Checklist - [ ] Remove all `"use server"` and `"use client"` directives - [ ] Remove `next.config.js` / `next.config.ts` - [ ] Remove `app/` directory (replaced by `src/routes/`) - [ ] Remove `middleware.ts` (replaced by `src/start.ts`) - [ ] Verify no `next/*` imports remain - [ ] Run `npm run dev` and check all routes - [ ] Verify server-only code is inside `createServerFn` (not bare in components/loaders) - [ ] Check that `<Scripts />` is in the root route `<body>` ## Common Mistakes ### 1. CRITICAL: Keeping Server Component mental model ```tsx // WRONG — treating component as server-only (Next.js habit) function PostsPage() { const posts = await db.posts.findMany() // fails on client return <div>{posts.map(...)}</div> } // CORRECT — use server function + loader const getPosts = createServerFn({ method: 'GET' }).handler(async () => { return db.posts.findMany() }) export const Route = createFileRoute('/posts')({ loader: () => getPosts(), component: PostsPage, }) ``` ### 2. CRITICAL: Using "use server" directive ```tsx // WRONG — "use server" is Next.js/React pattern
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub