
Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.
Migration Pages Router vers App Router : notre plan en 6 semaines sur 80 routes
- nextjs
Comment on a planifié et exécuté la migration d'un projet Next.js de 80 routes depuis Pages Router vers App Router en 6 semaines, sans régresser en production.
Migration Pages Router vers App Router : notre plan en 6 semaines sur un projet de 80 routes
La bonne nouvelle : Next.js supporte les deux routers en parallèle dans le même projet. La mauvaise : cette cohabitation a ses propres pièges, et une migration partielle qui s'étire est pire qu'un statu quo assumé.
Ce retour concerne un projet réel, application SaaS avec 80 routes, authentification par session, CMS headless, et une équipe de 4 développeurs.
Pourquoi migrer maintenant
On avait deux raisons concrètes. D'abord, les nouvelles features demandées, partial prerendering, streaming de sections lentes, nécessitaient App Router. Ensuite, Pages Router n'évolue plus activement : les nouvelles APIs Next.js ciblent App Router en priorité.
La question n'était pas "est-ce qu'on migre" mais "comment on migre sans casser la prod".
Semaine 1 : inventaire et classification
On n'a pas touché au code. On a inventorié les 80 routes selon trois critères :
- Type de rendu actuel : SSG / SSR / ISR / client-side
- Dépendances Pages Router spécifiques :
getServerSideProps,getStaticProps,useRouter(Pages),_app.tsx,_document.tsx - Criticité business : pages générant du trafic, pages dans le tunnel de conversion, pages admin internes
Résultat de l'inventaire : 34 routes pouvaient migrer mécaniquement (pas de dépendances complexes), 31 nécessitaient une adaptation, 15 avaient des cas limites à analyser individuellement.
On a aussi listé les packages tiers utilisés et vérifié leur compatibilité avec les Server Components. Deux librairies d'analytics avaient des problèmes connus avec le contexte App Router, on a ouvert les issues upstream avant de démarrer.
Semaine 2 : mise en place de la cohabitation
Next.js permet de faire cohabiter pages/ et app/ dans le même projet. On a créé le dossier app/ avec uniquement un layout racine vide et une route de test /app-test.
src/
pages/ # tout le projet existant, intact
app/
layout.tsx # layout racine minimal
app-test/
page.tsx # route de validationOn a validé que la cohabitation ne cassait rien en prod avant d'aller plus loin. C'est une étape qu'on recommande de ne pas sauter, on a eu une collision sur la gestion des headers de session entre _app.tsx et layout.tsx qu'on a résolue ici plutôt qu'au milieu de la migration.
Semaine 3 : migration du layout et de l'auth
Le layout global et la gestion d'authentification sont les fondations. On les a migrés en premier.
_app.tsx contenait la logique de session (next-auth), les providers React (theme, toasts, analytics), et l'import des styles globaux. La migration vers app/layout.tsx a pris une journée entière, pas à cause de la complexité technique, mais parce qu'on a dû identifier quels providers nécessitaient "use client" et lesquels pouvaient rester côté serveur.
// app/layout.tsx
import { Providers } from "./_providers"
import "./globals.css"
export default function RootLayout({ children }) {
return (
<html lang="fr">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
// app/_providers.tsx, client component qui regroupe les providers interactifs
"use client"
export function Providers({ children }) {
return (
<ThemeProvider>
<ToastProvider>
{children}
</ToastProvider>
</ThemeProvider>
)
}L'authentification avec next-auth v5 (compatible App Router) a nécessité une mise à jour de version. On a gelé cette mise à jour en semaine 1 pour ne pas cumuler les changements.
Semaines 4 et 5 : migration par lot
On a migré les routes par ordre de criticité inverse, les moins critiques d'abord. Chaque lot suivait le même processus :
- Créer la route dans
app/ - La tester manuellement sur la branche de migration
- Écrire ou adapter les tests de smoke correspondants
- Supprimer la route de
pages/ - Merger et déployer
On n'attendait pas la fin de la migration pour déployer. On déployait chaque lot en prod. C'est plus de déploiements, mais c'est moins de risque par déploiement.
Les routes avec getServerSideProps ont été les plus directes à migrer :
// Avant (pages/products/[id].tsx)
export async function getServerSideProps({ params }) {
const product = await getProduct(params.id)
return { props: { product } }
}
export default function ProductPage({ product }) { ... }
// Après (app/products/[id]/page.tsx)
export default async function ProductPage({ params }) {
const { id } = await params
const product = await getProduct(id)
return <ProductView product={product} />
}Les routes avec getStaticProps + getStaticPaths ont demandé un peu plus d'attention, il fallait transposer la logique vers generateStaticParams et choisir explicitement la stratégie de revalidation.
Semaine 6 : les 15 routes complexes
Les cas limites étaient principalement :
Routes avec middleware complexe. Certaines pages avaient des middlewares pages/api/ liés. On les a conservés temporairement et pointé dessus depuis les Server Actions ou les nouvelles routes API.
Routes avec state côté client complexe. Deux routes avaient des formulaires multi-étapes entièrement gérés côté client avec des librairies form lourdes. On les a conservées telles quelles en tant que Client Components, la migration vers des Server Components aurait changé le comportement, pas juste la structure.
Le router.push de Pages vs navigation App Router. L'API useRouter de Pages Router n'est pas compatible App Router. On a fait un audit systématique des import { useRouter } from 'next/router' pour les remplacer par import { useRouter } from 'next/navigation'. Les comportements diffèrent légèrement sur certains cas de navigation, on a eu une régression sur les redirections post-login qu'on a capturée grâce aux tests.
Résultat
Six semaines, zéro incident P1. On a eu deux régressions mineures capturées par les tests avant d'atteindre la prod.
Le bilan technique : le bundle client a diminué de 22% (les Server Components ne contribuent pas au bundle JS). Le TTFB médian a baissé de 15% sur les routes les plus visitées.
Ce qu'on referait différemment : démarrer l'inventaire deux semaines avant le début officiel de la migration. L'analyse des dépendances tierces et des cas limites prend plus de temps qu'on ne l'anticipe.
Ce qu'on ne referait pas : essayer de migrer l'authentification et les layouts en même temps que les premières routes. Séparer ces deux chantiers nous a évité des conflits difficiles à déboguer.
La question ouverte : comment gérer proprement la cohabitation des configurations de cache entre les routes Pages Router et App Router quand les deux sont en prod simultanément ? On n'a pas trouvé de documentation officielle claire là-dessus.