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.

소스 정보

저장소
TanStack/router
최근 소스 활동
2026년 7월 26일 16:53
감지된 SKILL.md 언어
영어
스타
15,202
포크
1,900

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기