Frontend & UISûr100100/100
Migrate From Nextjs (TanStack)
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.
ou envoie-le directement à ton agent.
Installer dans ton projet
$ curl -fsSL https://arboristhree.xyz/cli/install-skill.mjs -o /tmp/arboris-install-skill.mjs && node /tmp/arboris-install-skill.mjs migrate-from-nextjs__tanstack-router__packages-react-start-skills-lifecycle-fd68216c23a1TanStack
TanStack/router
Contenu à copier
---
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:
origin: TanStack
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
'use server'
export async function myAction() { ... }
// CORRECT — use createServerFn
export const myAction = createServerFn({ method: 'POST' })
.handler(async () => { ... })
```
### 3. HIGH: Interpolating params into Link href
```tsx
// WRONG — Next.js pattern
<Link to={`/posts/${post.id}`}>View</Link>
// CORRECT — TanStack Router pattern
<Link to="/posts/$postId" params={{ postId: post.id }}>View</Link>
```
## Cross-References
- [react-start](../../react-start/SKILL.md) — full React Start setup
- `@tanstack/start-client-core#start-core/server-functions` — server function patterns
- `@tanstack/start-client-core#start-core/execution-model` — isomorphic executionColle ce Markdown dans ton agent ou utilise les boutons ci-dessus pour l’écrire dans ton projet.
Ce que fait Migrate From Nextjs (TanStack)
[ ] **Create a migration branch**
[ ] **Install TanStack Start**
[ ] **Remove Next.js**
Dynamic segments use `$param` not `[param]`
Comment utiliser Migrate From Nextjs (TanStack)
1
Copie le prompt
Un clic copie le prompt packagé (ou l'envoie à ton agent).
2
L'agent installe le skill
Il ajoute le SKILL.md et ses ressources à ton projet.
3
Activation automatique
Le skill s'active dès que le contexte correspond.
Déclencheurs pour Migrate From Nextjs (TanStack)
Dis simplement à ton agent quelque chose comme :
Applique le skill migrate-from-nextjs__tanstack-router__packages-react-start-skills-lifecycle-fd68216c23a1 à cette tâche
Utilise migrate-from-nextjs__tanstack-router__packages-react-start-skills-lifecycle-fd68216c23a1 pour améliorer cette implémentation
Passe en revue ce sujet avec migrate-from-nextjs__tanstack-router__packages-react-start-skills-lifecycle-fd68216c23a1
Skills liés à Migrate From Nextjs (TanStack)
accessibility
Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels, accessibility traits and hints, focus management, contrast, target size, and screen-reader support. Use when building or auditing UI for accessibility compliance, keyboard navigation, or screen-reader support.Accessibility Pass (Microsoft)
Use this skill whenever the user asks to check, review, or fix the accessibility of a PowerPoint deck, Word document, HTML page, or Markdown file, and before handing over any deck or document this agent just generated, so it does not ship with missing alt text, untitled slides, or unreadable colour contrast.Add Connector (Microsoft)
Use when adding a Power Platform connector to an Expo/React Native Power Apps mobile app and no dedicated mobile connector skill exists.