ITSkillsCenter
Blog

Migration depuis Next.js App Router vers TanStack Start : plan et pièges en 2026

12 min de lecture

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 fetch ou 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.

  1. Toutes les routes app/**/page.tsx avec leur chemin URL.
  2. Tous les layout.tsx et leur portée.
  3. Toutes les Server Actions (fichiers ou inline avec 'use server').
  4. Toutes les routes API app/api/**/route.ts.
  5. Toutes les middlewares (middleware.ts à la racine).
  6. Tous les composants utilisant des hooks Next-spécifiques (useRouter, usePathname, useSearchParams).
  7. 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

Ressources

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité