Migrer une application Next.js App Router en production vers TanStack Start n’est pas une décision à prendre à la légère. Il faut peser les bénéfices (lisibilité, type-safety totale, portabilité multi-runtime) face aux coûts (réécriture de patterns server-first, perte temporaire d’écosystème, courbe d’apprentissage). Ce tutoriel propose un plan de migration progressive, une table d’équivalences API détaillée, des stratégies pour porter les Server Components, et la gestion des redirections SEO pour ne perdre aucun jus de référencement. L’objectif : sortir de la migration en deux à six mois selon la taille de l’app, sans interruption de service.
Article de la série autour de TanStack Start en production 2026.
Prérequis
Le tutoriel s’adresse aux équipes qui maîtrisent déjà Next.js App Router et qui ont une raison concrète de migrer. Il ne s’agit pas d’un comparatif théorique mais d’un plan d’exécution. Si vous découvrez TanStack Start, lisez d’abord l’article principal sur TanStack Start en production 2026.
- Une application Next.js 14 ou 15 en production avec App Router.
- Au moins un développeur sénior à temps plein sur la migration.
- Connaissance pratique de Server Components, Server Actions et du cache fetch Next.js.
- Un environnement de staging isolé pour tester sans casser la prod.
- Accès aux logs de production pour identifier les routes critiques.
Étape 1 — Évaluer la pertinence de la migration
Avant la moindre ligne de code, on quantifie le retour sur investissement. Toutes les apps Next.js ne gagnent pas à migrer ; certaines sont parfaitement bien sur App Router. Le critère de décision tient en trois questions concrètes.
- L’équipe se bat-elle régulièrement avec le cache
fetchou la directive'use server'? Si oui, TanStack Start résout cette douleur par une explicitude qui rend l’état de l’app lisible. - Le projet doit-il quitter Vercel pour Cloudflare, Bun, AWS Lambda ou un VPS ? Le runtime Nitro de Start déploie partout sans bidouille, là où App Router demande des contournements.
- Le projet a-t-il besoin d’une type-safety stricte sur les routes et les search params ? TanStack Router fait ça nativement ; Next.js demande des plugins TypeScript externes plus fragiles.
Si la réponse à au moins deux des trois questions est oui, la migration vaut le coup. Sinon, restez sur Next.js — l’écosystème est plus mature et la dette technique d’une migration sans bénéfice clair n’a pas de sens.
Étape 2 — Cartographier l’application existante
On commence par lister exhaustivement ce qui doit être migré. Un tableur ou un fichier Markdown suffit ; l’objectif est de ne rien oublier. Sept catégories à inventorier.
- Toutes les routes
app/**/page.tsxavec leur chemin URL. - Tous les
layout.tsxet leur portée. - Toutes les Server Actions (fichiers ou inline avec
'use server'). - Toutes les routes API
app/api/**/route.ts. - Toutes les middlewares (
middleware.tsà la racine). - Tous les composants utilisant des hooks Next-spécifiques (
useRouter,usePathname,useSearchParams). - Toutes les configurations spéciales (
next.config.js, redirects, rewrites, image optimization).
Cet inventaire devient le backlog de la migration. Pour une app moyenne de 30 routes, comptez deux à trois jours pour le faire correctement. C’est un investissement qui paye au centuple ensuite.
Étape 3 — Choisir une stratégie de bascule
Trois stratégies ont fait leurs preuves en 2026. Le choix dépend de la taille de l’app, du SEO et de la tolérance au risque.
| Stratégie | Quand l’utiliser | Durée typique |
|---|---|---|
| Strangler avec reverse proxy | App critique, SEO sensible, équipe taille moyenne | 4 à 8 mois |
| Coexistence sous-domaine | Nouvelle feature majeure isolée, app legacy figée | 2 à 4 mois |
| Réécriture par module | App jeune, équipe greenfield-friendly | 2 à 5 mois |
La stratégie strangler reste la plus sûre. Un reverse proxy (Nginx, Cloudflare Workers, Caddy) route certaines URLs vers la nouvelle app TanStack Start et le reste vers l’ancienne app Next.js. La bascule devient un changement de config dans le proxy, pas un déploiement risqué. Vous testez en interne, puis en bêta sur 5 % du trafic, puis 100 % quand la nouvelle route est validée.
Étape 4 — Bootstrapper le projet TanStack Start
On crée un nouveau projet TanStack Start dans un dossier séparé du projet Next.js existant. Pas de monorepo obligatoire à ce stade ; deux dépôts indépendants simplifient la coexistence. Suivez le tutoriel d’installation TanStack Start v1 pour la mise en route.
npm create tanstack@latest my-app-v2
cd my-app-v2
npm install
npm run dev
Une fois le projet vide qui tourne sur localhost:3000, on installe les dépendances que l’app legacy utilisait également : Drizzle ou Prisma, Zod, le client de votre base, les bibliothèques UI (shadcn/ui, Mantine, Material UI). À ce stade, vous avez un canevas vierge prêt à recevoir le code migré.
Étape 5 — Convertir le routage app/ vers src/routes/
Le mapping est presque mécanique. Voici la table de correspondance qui couvre 95 % des cas.
| Next.js App Router | TanStack Router |
|---|---|
app/page.tsx |
src/routes/index.tsx |
app/about/page.tsx |
src/routes/about.tsx ou src/routes/about/index.tsx |
app/blog/[slug]/page.tsx |
src/routes/blog/$slug.tsx |
app/[...slug]/page.tsx |
src/routes/$.tsx |
app/layout.tsx (root) |
src/routes/__root.tsx |
app/(marketing)/page.tsx |
src/routes/(marketing)/index.tsx |
app/dashboard/layout.tsx |
src/routes/dashboard/route.tsx |
app/_protected/layout.tsx (pathless) |
src/routes/_protected.tsx |
Un piège classique : Next utilise [slug] avec crochets, TanStack Router utilise $slug avec dollar. Une recherche-remplacement rapide sur le projet automatise 90 % du travail. Pour les détails, voir le tutoriel sur le file-based routing TanStack Router.
Étape 6 — Porter les Server Components
C’est la partie la plus intéressante. TanStack Start n’utilise pas les React Server Components ; il propose une approche différente. Les patterns courants ont des équivalents directs.
Server Component qui fait fetch et rend du JSX : devient un composant React standard avec un loader de route qui prefetch et un useSuspenseQuery qui consomme. Voir le tutoriel TanStack Query SSR pour le pattern complet.
// AVANT (Next.js Server Component)
async function PostList() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
// APRÈS (TanStack Start)
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
const postsQuery = queryOptions({
queryKey: ['posts'],
queryFn: () => fetch('https://api.example.com/posts').then(r => r.json()),
})
export const Route = createFileRoute('/posts/')({
loader: ({ context }) => context.queryClient.prefetchQuery(postsQuery),
component: PostList,
})
function PostList() {
const { data } = useSuspenseQuery(postsQuery)
return <ul>{data.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}
Le code devient légèrement plus verbeux mais radicalement plus traçable : la queryKey est explicite, le cache est inspectable dans React Query Devtools, l’invalidation après mutation est triviale. Pour les Server Components qui appelaient directement la base de données, le port est encore plus naturel : on les transforme en server functions.
Étape 7 — Convertir les Server Actions en server functions
La directive 'use server' de Next.js est remplacée par createServerFn de TanStack Start. La conversion est mécanique. Le détail complet du pattern est dans le tutoriel sur les server functions TanStack Start.
// AVANT (Next.js Server Action)
'use server'
import { z } from 'zod'
export async function createPost(data: { title: string }) {
const parsed = z.object({ title: z.string().min(3) }).parse(data)
return await db.insert(posts).values(parsed).returning()
}
// APRÈS (TanStack Start server function)
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
export const createPost = createServerFn({ method: 'POST' })
.inputValidator(z.object({ title: z.string().min(3) }))
.handler(async ({ data }) => {
return await db.insert(posts).values(data).returning()
})
L’appel côté composant devient await createPost({ data: { title: 'test' } }) au lieu d’un appel direct. Vous gagnez la validation explicite, la possibilité d’attacher un middleware d’authentification, et l’inférence de type bidirectionnelle. Pour les invalidations de cache, on remplace revalidatePath par queryClient.invalidateQueries.
Étape 8 — Migrer les routes API
Les app/api/**/route.ts de Next.js deviennent des routes TanStack Router avec un handler serveur explicite. La syntaxe est légèrement différente mais le mental model est le même.
// AVANT (Next.js app/api/posts/route.ts)
export async function GET(request: Request) {
const posts = await db.select().from(postsTable)
return Response.json(posts)
}
// APRÈS (TanStack Start src/routes/api/posts.ts)
import { createFileRoute } from '@tanstack/react-router'
import { db } from '~/db/client'
import { posts as postsTable } from '~/db/schema'
export const Route = createFileRoute('/api/posts')({
server: {
handlers: {
GET: async () => {
const posts = await db.select().from(postsTable)
return Response.json(posts)
},
},
},
})
Les routes catch-all (app/api/[...slug]/route.ts) deviennent src/routes/api/$.ts. La logique interne reste identique : c’est juste l’enveloppe qui change.
Étape 9 — Remplacer les hooks Next-spécifiques
Les hooks next/navigation ont des équivalents directs dans @tanstack/react-router. Le tableau suivant accélère le portage.
| Next.js | TanStack Router |
|---|---|
useRouter() |
useRouter() (API différente) |
router.push('/x') |
navigate({ to: '/x' }) |
usePathname() |
useLocation().pathname |
useSearchParams() |
useSearch() (typé) |
useParams() |
useParams({ from: '/route/$id' }) |
redirect('/login') |
throw redirect({ to: '/login' }) |
notFound() |
throw notFound() |
Les Link de next/link deviennent Link de @tanstack/react-router avec une prop to typée. C’est un gain ergonomique : tapez to="/posts/$id" et l’éditeur vous demande automatiquement le param id. La faute de frappe sur une URL devient impossible.
Étape 10 — Gérer les redirections SEO sans perdre de jus
Si l’URL change pendant la migration (par exemple /blog/[slug] reste mais /products/[id] devient /produits/[id]), on doit poser des redirections 301 pour que Google transfère le ranking. La méthode dépend de votre proxy.
Sur Cloudflare Workers, créez une route catch dans le Worker frontal qui inspecte le path et renvoie un 301 vers la nouvelle URL. Sur Nginx, ajoutez les directives location ~* /old-path { return 301 /new-path; }. Sur Vercel, le fichier vercel.json avec la clé redirects fait l’affaire.
// Exemple Worker frontal pour redirections
const REDIRECTS: Record<string, string> = {
'/products': '/produits',
'/blog/old-slug': '/blog/new-slug',
}
export default {
async fetch(request: Request) {
const url = new URL(request.url)
if (REDIRECTS[url.pathname]) {
return Response.redirect(`${url.origin}${REDIRECTS[url.pathname]}`, 301)
}
return fetch(request)
},
}
Surveillez ensuite Google Search Console pendant deux à quatre semaines après chaque vague de bascule. Les Crawl Stats doivent rester stables et les pages migrées doivent être ré-indexées dans les jours qui suivent. Pour accélérer, soumettez les nouvelles URLs via IndexNow et les anciennes pour réévaluation via l’inspection d’URL.
Différences à retenir
| Aspect | Next.js App Router | TanStack Start |
|---|---|---|
| Métadonnées de page | Export metadata |
head() dans la route |
| Loading UI | loading.tsx |
pendingComponent dans la route |
| Erreur UI | error.tsx |
errorComponent dans la route |
| Image optimisation | next/image |
Solution tierce (unpic, image-loader Cloudflare) |
| Font optimisation | next/font |
fontsource ou self-hosted classique |
| Middleware Edge | middleware.ts |
Worker Cloudflare en amont ou middleware route |
| Streaming | RSC streaming natif | defer dans le loader |
| ISR | revalidate sur fetch |
staleTime Query + cache CDN |
Quelques fonctionnalités Next n’ont pas d’équivalent direct (l’Image avec optimisation automatique notamment). Pour celles-ci, la communauté propose des alternatives matures, ou vous pouvez utiliser un service externe (Cloudflare Images, ImgIX, Bunny.net).
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Hydratation mismatch sur dates | Composants utilisant new Date() à la frontière SSR/client |
Mover dans useEffect ou utiliser suppressHydrationWarning |
| Cache Query reset à chaque navigation | QueryClient instancié hors du router | Toujours via createRouter |
| Server Action portée mais inutilisée | Composant migré utilise encore useFormState |
Remplacer par useMutation de Query |
| 404 sur /api/auth/* après migration auth | Route catch-all manquante | Créer src/routes/api/auth/$.ts |
| Métadonnées disparues après portage | Export metadata non remplacé par head() |
Utiliser head() dans createFileRoute |
| Bundle plus gros qu’attendu | Imports dynamiques perdus à la conversion | Activer autoCodeSplitting: true dans le plugin router |
FAQ
Combien de temps prend une migration moyenne ? Pour une app de 30 routes avec 10 server actions et un module d’auth, comptez 6 à 12 semaines à temps plein pour un développeur sénior. Une équipe de deux peut diviser ce temps presque par deux si les modules sont bien séparés.
Peut-on migrer progressivement avec deux apps en parallèle ? Oui, c’est même recommandé. Le strangler pattern derrière un reverse proxy permet de tester chaque route sur de petits pourcentages de trafic avant de basculer 100 %.
Comment porter next/image ? Pour des images servies depuis un domaine custom, utilisez le composant Image de la bibliothèque unpic, qui supporte les principaux providers (Cloudinary, ImageKit, Cloudflare Images). Pour des assets statiques, un <img> standard avec loading="lazy" et srcset couvre 90 % des besoins.
Et le SEO sera-t-il préservé ? Oui, à condition de poser les redirections 301 pour toute URL qui change et de surveiller Search Console. La structure SSR de TanStack Start est aussi indexable que celle de Next.js.
Vaut-il mieux attendre la 1.0 stable de TanStack Start ? Pas forcément. La RC est annoncée comme la build qui sortira en 1.0. Migrer maintenant donne une avance de 6 à 12 mois et ferme la dette technique liée à App Router.
Pour aller plus loin
- L’article principal : TanStack Start en production 2026.
- Pour la nouvelle architecture de routage : file-based routing avec TanStack Router.
- Pour porter les Server Actions : server functions TanStack Start.
- Pour la couche données : TanStack Query SSR et hydratation.
- Pour migrer aussi l’auth : better-auth dans TanStack Start.
- Pour redéployer ailleurs que Vercel : déploiement Cloudflare Workers.
Ressources
- Documentation officielle TanStack Start : tanstack.com/start/latest/docs/framework/react/overview
- Documentation TanStack Router : tanstack.com/router/latest/docs
- Annonce de la Release Candidate v1 : tanstack.com/blog/announcing-tanstack-start-v1
- Documentation Next.js App Router (référence pour comparer) : nextjs.org/docs/app