Next.js App Router en pratique : groupes de routes et layouts imbriqués pour les grands projets

Dans l’explorateur VS Code, le dossier app affiche plus de 120 dossiers. Pour trouver la gestion des utilisateurs du back-office, on passe cinq minutes entre dashboard-user-list, admin-users et backend-user-management — tous se ressemblent.
Lors d’une revue de code, un collègue ajoute la route /about, qui entre en conflit avec celle du marketing déposée la semaine dernière. Deux visions du « about » s’affrontent sans solution évidente.
Au départ, une dizaine de pages en structure plate tenait la route. Six mois plus tard, le volume a décuplé : l’arborescence ressemble à un placard non rangé — tout est là, mais chaque recherche prend du temps.
Sur un projet Next.js avec plus de trois personnes et plus de 50 pages, ce scénario est fréquent. L’App Router propose quatre leviers : groupes de routes, layouts imbriqués, routes parallèles et routes interceptées. Les tutoriels « Hello World » peinent à montrer l’usage en production.
Cet article suit un exemple e-commerce réel : comment appliquer ces quatre mécanismes et repasser d’une arborescence chaotique à une structure maintenable.
Retour sur les douleurs — trois limites de l’arborescence classique
L’enfer de l’arborescence plate
Ancienne structure type :
app/
├── page.tsx # Accueil
├── about/page.tsx # À propos
├── products/page.tsx # Liste produits
├── product-detail/[id]/page.tsx
├── cart/page.tsx
├── checkout/page.tsx
├── dashboard/page.tsx # Accueil back-office
├── dashboard-users/page.tsx
├── dashboard-users-active/page.tsx
├── dashboard-users-blocked/page.tsx
├── dashboard-orders/page.tsx
├── dashboard-orders-pending/page.tsx
├── dashboard-settings/page.tsx
├── auth-login/page.tsx # Connexion
├── auth-register/page.tsx
└── ... (80+ dossiers)
Les URL deviennent bizarres : /dashboard-users-active au lieu de /dashboard/users/active. Les préfixes masquent le problème sans le résoudre.
Impossible de distinguer d’un coup d’œil front, back-office et auth. Un nouvel arrivant met des jours à s’orienter.
Layouts dupliqués et maintenance lourde
Front et back-office n’ont pas la même coque : navigation + pied de page côté public, sidebar + droits côté admin. Ancienne approche — importer le layout dans chaque page :
// app/dashboard-users/page.tsx
import DashboardLayout from '@/components/DashboardLayout'
export default function UsersPage() {
return (
<DashboardLayout>
<div>Contenu gestion utilisateurs</div>
</DashboardLayout>
)
}
Risques : oubli du layout sur une nouvelle page, mélange de DashboardLayout et AdminLayout, et vingt fichiers à vérifier pour un changement de sidebar.
Modales et routing
Besoin produit : clic sur un produit dans la liste → modale avec détail, URL /product/123 partageable. État client + manipulation manuelle de l’URL : code fragile ; au refresh, la modale disparaît.
Deux implémentations (modale + page pleine) dupliquent la logique. L’expérience type Instagram (liste → modale, refresh → page pleine, lien partagé → page pleine) est pénible sans routing dédié.
Groupes de routes (Route Groups) — organiser sans changer l’URL
Définition
Un nom de dossier entre parenthèses, par ex. (marketing), n’apparaît pas dans l’URL :
app/
├── (marketing)/ # Pages marketing
│ ├── layout.tsx # Layout front
│ ├── page.tsx # URL: /
│ ├── about/page.tsx # URL: /about
│ └── products/page.tsx # URL: /products
├── (shop)/ # E-commerce
│ ├── layout.tsx
│ ├── cart/page.tsx # URL: /cart
│ └── checkout/page.tsx # URL: /checkout
└── (dashboard)/ # Back-office
├── layout.tsx
├── dashboard/page.tsx # URL: /dashboard
├── users/page.tsx # URL: /users
└── orders/page.tsx # URL: /orders
(marketing)/about/page.tsx reste /about, pas /marketing/about.
La valeur est l’organisation du code et la séparation des layouts, pas la forme de l’URL.
Cas : découpage par équipe
Trois équipes — marketing (site vitrine), produit (boutique), back-office :
app/
├── (team-marketing)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/page.tsx
│ └── pricing/page.tsx
├── (team-product)/
│ ├── layout.tsx
│ ├── products/page.tsx
│ └── product/[id]/page.tsx
└── (team-backend)/
├── layout.tsx
├── dashboard/page.tsx
└── admin/page.tsx
Effets observés :
- Moins de conflits Git — chaque équipe dans son groupe.
- Revues plus claires — l’impact d’une PR se lit sur le groupe touché.
- Layouts dédiés — pas d’import manuel page par page.
Cas : découpage par type de layout
app/
├── (with-nav)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/page.tsx
│ └── products/page.tsx
├── (fullscreen)/
│ ├── layout.tsx
│ └── video/[id]/page.tsx
└── (auth)/
├── layout.tsx
├── login/page.tsx
└── register/page.tsx
Login sans barre de navigation ; vidéo en plein écran dans son groupe.
Points d’attention
Deux groupes ne peuvent pas exposer la même URL :
❌ (marketing)/about/page.tsx → /about
❌ (shop)/about/page.tsx → /about (conflit)
Planifier les chemins à l’avance, ou différencier (/about-us vs /about-product).
Nommer (marketing), (dashboard), (auth) — pas (group1) ni (temp).
Layouts imbriqués (Nested Layouts) — héritage automatique
Fonctionnement
Exemple back-office à trois niveaux : barre + sidebar (tous), onglets utilisateurs (module), contenu (page). Un layout.tsx par niveau :
app/(dashboard)/
├── layout.tsx # Niveau 1 : barre + sidebar
├── users/
│ ├── layout.tsx # Niveau 2 : onglets utilisateurs
│ ├── active/page.tsx # /users/active
│ └── blocked/page.tsx # /users/blocked
└── orders/
├── layout.tsx # Niveau 2 : onglets commandes
├── pending/page.tsx
└── completed/page.tsx
Pour /users/active :
DashboardLayout (niveau 1)
└─ UsersLayout (niveau 2)
└─ ActiveUsersPage
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard-container">
<TopBar />
<div className="content-area">
<Sidebar />
<main>{children}</main>
</div>
</div>
)
}
// app/(dashboard)/users/layout.tsx
export default function UsersLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="users-section">
<div className="tabs">
<Link href="/users/active">Utilisateurs actifs</Link>
<Link href="/users/blocked">Utilisateurs bloqués</Link>
</div>
{children}
</div>
)
}
// app/(dashboard)/users/active/page.tsx
export default function ActiveUsersPage() {
return <div>Liste des utilisateurs actifs...</div>
}
La page n’importe plus le layout : Next.js compose la hiérarchie.
Rendu partiel (Partial Rendering)
Passage de « actifs » à « bloqués » :
- Le layout niveau 1 (barre, sidebar) ne se re-rend pas.
- Le layout niveau 2 (onglets) non plus.
- Seul le contenu de la page change.
Performance et état client conservé (ex. texte dans la recherche de la sidebar).
Navigation multi-niveaux
L’arborescence reflète l’UI :
app/(dashboard)/
├── layout.tsx
├── users/
│ ├── layout.tsx
│ ├── active/page.tsx
│ └── blocked/page.tsx
└── orders/
├── layout.tsx
├── pending/page.tsx
└── completed/page.tsx
Astuce performance
Les layouts sont des Server Components par défaut. Extraire l’interactif en composants client :
// app/(dashboard)/layout.tsx
import SearchBar from '@/components/SearchBar'
export default function DashboardLayout({ children }) {
return (
<div>
<SearchBar />
<main>{children}</main>
</div>
)
}
// components/SearchBar.tsx
'use client'
import { useState } from 'react'
export default function SearchBar() {
const [query, setQuery] = useState('')
// ...
}
Ajouter loading.tsx à chaque niveau pour des états de chargement indépendants.
Routes parallèles (Parallel Routes) — plusieurs pages à la fois
Problème
Un tableau de bord affiche analytics, équipe et notifications. Données indépendantes, latences différentes. Tout dans une page : un module lent bloque le reste.
Les routes parallèles découpent en slots @nom, chacun avec son loading.js / error.js.
Syntaxe de base
app/dashboard/
├── layout.tsx
├── @analytics/page.tsx
├── @team/page.tsx
├── @notifications/page.tsx
└── page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
notifications,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
notifications: React.ReactNode
}) {
return (
<div className="dashboard-grid">
<div className="main-content">{children}</div>
<div className="top-panels">
<div className="panel">{analytics}</div>
<div className="panel">{team}</div>
</div>
<div className="bottom-panel">{notifications}</div>
</div>
)
}
Rendu conditionnel
// app/dashboard/layout.tsx
import { auth } from '@/lib/auth'
export default async function DashboardLayout({
analytics,
team,
notifications,
}) {
const user = await auth()
const isAdmin = user?.role === 'admin'
return (
<div className="dashboard-grid">
<div>{analytics}</div>
{isAdmin && <div>{team}</div>}
<div>{notifications}</div>
</div>
)
}
Rôle de default.tsx
Navigation vers une route sans page pour un slot → erreur sans default.tsx :
// app/dashboard/@team/default.tsx
export default function Default() {
return null
}
Quand s’en servir
Pertinent pour : dashboards multi-panneaux, A/B test par slot, rendu selon les rôles. Pour une simple pile verticale sans chargement indépendant, une page unique suffit.
Routes interceptées (Intercepting Routes) — modales et URL partageables
Expérience type Instagram
Feed → clic photo → modale + URL /photo/abc123. Refresh → page pleine ; lien partagé → page pleine ; fermeture → retour au feed. URL utile sans perdre le contexte de navigation.
Syntaxe
(.)— même segment d’URL(..)— parent(..)(..)— deux niveaux au-dessus(...)— depuis la racine de l’app
app/
├── products/
│ ├── page.tsx
│ └── (..)product/[id]/page.tsx # Modale en navigation client
└── product/
└── [id]/page.tsx # Page complète
Clic depuis /products vers /product/123 : version interceptée (modale). Accès direct ou refresh : product/[id]/page.tsx.
Exemple modale produit
app/
├── (shop)/
│ └── products/
│ ├── page.tsx
│ └── (..)product/[id]/page.tsx
└── product/
└── [id]/page.tsx
// app/(shop)/products/(..)product/[id]/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import Modal from '@/components/Modal'
import ProductDetail from '@/components/ProductDetail'
export default function ProductModal({
params
}: {
params: { id: string }
}) {
const router = useRouter()
return (
<Modal onClose={() => router.back()}>
<ProductDetail id={params.id} />
</Modal>
)
}
// app/product/[id]/page.tsx
import ProductDetail from '@/components/ProductDetail'
export default function ProductPage({
params
}: {
params: { id: string }
}) {
return (
<div className="product-page">
<ProductDetail id={params.id} />
</div>
)
}
ProductDetail est réutilisé ; seul le conteneur change.
Avec routes parallèles
app/(shop)/products/
├── layout.tsx
├── page.tsx
├── @modal/
│ ├── (..)product/[id]/page.tsx
│ └── default.tsx
// app/(shop)/products/layout.tsx
export default function ProductsLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
// app/(shop)/products/@modal/default.tsx
export default function Default() {
return null
}
Limites
- Interception uniquement en navigation client.
- Deux routes à maintenir (modale + page), composants partagés recommandés.
(..)suit les segments d’URL, pas le chemin disque ; les groupes(shop)n’apparaissent pas dans l’URL.
Quand l’utiliser
Adapté : galeries, fiche produit en modale, login en overlay avec /login direct. Inutile si la modale ne doit pas changer l’URL ni être deep-linkée.
Mise en pratique — arborescence e-commerce complète
Besoins
Public : accueil, à propos ; catalogue ; panier, checkout ; détail produit en modale.
Admin : dashboard multi-modules ; utilisateurs (actifs / bloqués) ; commandes (en attente / terminées).
Auth : login / register, layout minimal.
Arborescence cible
app/
├── layout.tsx
│
├── (marketing)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/page.tsx
│ └── pricing/page.tsx
│
├── (shop)/
│ ├── layout.tsx
│ ├── products/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── @modal/
│ │ ├── (..)product/[id]/page.tsx
│ │ └── default.tsx
│ ├── cart/page.tsx
│ └── checkout/page.tsx
│
├── product/
│ └── [id]/page.tsx
│
├── (dashboard)/
│ ├── layout.tsx
│ ├── dashboard/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── @analytics/
│ │ │ ├── page.tsx
│ │ │ ├── loading.tsx
│ │ │ └── default.tsx
│ │ ├── @team/
│ │ │ ├── page.tsx
│ │ │ ├── loading.tsx
│ │ │ └── default.tsx
│ │ └── @notifications/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── default.tsx
│ ├── users/
│ │ ├── layout.tsx
│ │ ├── active/page.tsx
│ │ └── blocked/page.tsx
│ └── orders/
│ ├── layout.tsx
│ ├── pending/page.tsx
│ └── completed/page.tsx
│
└── (auth)/
├── layout.tsx
├── login/page.tsx
└── register/page.tsx
Comparaison
| Dimension | Structure plate | Groupes + layouts imbriqués |
|---|---|---|
| Recherche fichier | 100+ entrées, préfixes | Groupes par métier |
| Layouts | Import manuel par page | Héritage, un fichier par niveau |
| Équipe | Un seul dossier app | Dossiers par équipe / zone |
| URL | Préfixes longs | Chemins courts (/users/active) |
| Modales | État client, refresh fragile | Routing + page pleine au refresh |
| Performance | Re-render des layouts | Rendu partiel |
Bénéfices constatés
- ~50 % plus rapide pour retrouver une page.
- Un seul layout à modifier pour tout le back-office.
- ~60 % moins de conflits Git entre équipes.
- Onboarding : l’arborescence explique l’architecture.
Conseils
- Refonte module par module, pas big bang.
- Noms de groupes explicites :
(marketing),(shop),(dashboard),(auth). - Documenter l’arborescence dans le README.
- Alias TypeScript :
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/app/*": ["./src/app/*"]
}
}
}
Bonnes pratiques et pièges
Nommage des groupes
Recommandé : (marketing), (dashboard) / (admin), (auth), (team-xxx), (feature-xxx).
À éviter : (group1), (temp), noms trop longs.
Conflits d’URL
❌ (marketing)/about/page.tsx
❌ (shop)/about/page.tsx
Planifier les routes, préfixer ou déplacer l’un des chemins.
Routes parallèles
Oui : panneaux indépendants, chargements séparés, permissions, A/B.
Non : contenu empilé simple sans besoin de streaming par zone.
En cas de doute, commencer sans routes parallèles.
Routes interceptées
Navigation client uniquement ; deux fichiers de route ; (..) = URL, pas dossiers disque.
Sans besoin de deep link, une modale client classique peut suffire.
Performance
- Garder les layouts en Server Components.
loading.tsxà chaque niveau utile.- Suspense avec streaming.
- Limiter l’imbrication à ~4 niveaux.
Migration depuis Pages Router
- Coexistence
app/etpages/, migration par zone. - D’abord layouts et groupes.
- Puis fetch / Server Components à la place de
getServerSideProps/getStaticProps. generateStaticParamspour les anciensgetStaticPaths.- Feature flags pour basculer progressivement.
Collaboration
Documenter les conventions, revues focalisées routing/layouts, règles ESLint optionnelles, nettoyage régulier des routes mortes.
Debug
React DevTools pour l’arbre, console.log dans les layouts, onglet Network, messages du terminal Next.js (conflits, layouts manquants).
Conclusion
Cent vingt dossiers à minuit pour trouver une page : ce n’est pas une fatalité.
- Groupes de routes : organisation et équipes sans URL verbeuses.
- Layouts imbriqués : plus d’imports répétés, un fichier par niveau de chrome.
- Routes parallèles : panneaux indépendants pour les dashboards.
- Routes interceptées : modales avec URL partageable et page pleine au refresh.
Commencer par le back-office avec groupes et layouts imbriqués, mesurer, puis étendre. Migration incrémentale, rollback facile.
Le schéma e-commerce ci-dessus peut servir de modèle. Bon courage pour une arborescence app/ enfin lisible.
Refonte complète de l’arborescence d’un grand projet Next.js
Réorganiser une arborescence confuse avec groupes de routes, layouts imbriqués, routes parallèles et interceptées
⏱️ Estimated time: 8 hr
- 1
Step 1: Analyser l’arborescence actuelle
Évaluer les problèmes :
• Compter les dossiers (au-delà de 50, envisager une refonte)
• Repérer les conflits d’URL
• Identifier les layouts dupliqués
• Noter les points de friction en équipe
Zones fonctionnelles :
• Marketing (accueil, à propos, tarifs)
• Boutique (produits, panier, commandes)
• Back-office (utilisateurs, commandes, paramètres) - 2
Step 2: Créer des groupes de routes par zone
Parenthèses pour les groupes :
• (marketing) : pages marketing
• (shop) : e-commerce
• (dashboard) : administration
À retenir :
• Le nom du groupe n’apparaît pas dans l’URL
• Une même URL ne peut pas exister dans deux groupes
• Chaque groupe peut avoir son layout.js - 3
Step 3: Concevoir la hiérarchie des layouts
Selon l’UI :
• Niveau 1 : app/layout.js (global)
• Niveau 2 : layout de zone (ex. shop/layout.js)
• Niveau 3 : layout de détail (ex. shop/products/[id]/layout.js)
Points clés :
• Chaque niveau n’ajoute que l’UI propre à ce niveau
• Héritage automatique des layouts parents
• Pas de re-render du layout au changement de page - 4
Step 4: Implémenter les routes parallèles (si besoin)
Créer des slots avec @ :
• @modal : modale
• @sales, @orders : modules indépendants du tableau de bord
Dans layout.js :
• export default function Layout({ children, modal })
• Rendu JSX : {modal}
Chaque slot peut avoir loading.js et error.js - 5
Step 5: Implémenter les routes interceptées (modales)
Routes interceptées :
• Sous @modal : (.)photos/[id]/page.js
• Page complète : photos/[id]/page.js
Syntaxe :
• (.) : même niveau d’URL
• (..) : niveau parent
• (...) : depuis la racine
Créer default.js qui retourne null - 6
Step 6: Tester et valider
Vérifier :
• Toutes les routes répondent
• Imbrication correcte des layouts
• Navigation fluide
• Modales OK
Outils :
• React DevTools (arbre des composants)
• console.log (re-renders)
• Network (requêtes)
• Avertissements dans le terminal Next.js
FAQ
Quand utiliser des groupes de routes ?
Les groupes de routes modifient-ils l’URL ?
Les layouts imbriqués dégradent-ils les performances ?
Différence entre routes parallèles et composants classiques ?
Comment choisir la syntaxe d’interception ?
Comment éviter les conflits de routes ?
Combien de temps pour refactorer un grand projet ?
9 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Routage avancé Next.js : groupes de routes, layouts imbriqués, routes parallèles et routes interceptées
Guide complet des quatre fonctionnalités de routage avancé Next.js : groupes de routes pour une arborescence claire, layouts imbriqués réutilisables, routes parallèles pour afficher plusieurs pages simultanément, routes interceptées pour des modales élégantes. Exemples de code complets et pièges à éviter.
Partie 4 sur 51
Suivant
Routes dynamiques Next.js et paramètres : guide complet de l'initiation au typage
Maîtrisez pas à pas le système de routes dynamiques de Next.js 14+. Paramètres dynamiques, routes catch-all, paramètres optionnels, quand utiliser generateStaticParams et pratiques TypeScript pour un typage sûr. Levez la confusion sur la récupération des paramètres, avec de nombreux exemples de code.
Partie 6 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire