Changer le thème

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

Easton editorial illustration: build pipeline conveyor

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 :

  1. Moins de conflits Git — chaque équipe dans son groupe.
  2. Revues plus claires — l’impact d’une PR se lit sur le groupe touché.
  3. 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).

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

  1. Interception uniquement en navigation client.
  2. Deux routes à maintenir (modale + page), composants partagés recommandés.
  3. (..) 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

DimensionStructure plateGroupes + layouts imbriqués
Recherche fichier100+ entrées, préfixesGroupes par métier
LayoutsImport manuel par pageHéritage, un fichier par niveau
ÉquipeUn seul dossier appDossiers par équipe / zone
URLPréfixes longsChemins courts (/users/active)
ModalesÉtat client, refresh fragileRouting + page pleine au refresh
PerformanceRe-render des layoutsRendu partiel

Bénéfices constatés

  1. ~50 % plus rapide pour retrouver une page.
  2. Un seul layout à modifier pour tout le back-office.
  3. ~60 % moins de conflits Git entre équipes.
  4. Onboarding : l’arborescence explique l’architecture.

Conseils

  1. Refonte module par module, pas big bang.
  2. Noms de groupes explicites : (marketing), (shop), (dashboard), (auth).
  3. Documenter l’arborescence dans le README.
  4. 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

  1. Garder les layouts en Server Components.
  2. loading.tsx à chaque niveau utile.
  3. Suspense avec streaming.
  4. Limiter l’imbrication à ~4 niveaux.

Migration depuis Pages Router

  1. Coexistence app/ et pages/, migration par zone.
  2. D’abord layouts et groupes.
  3. Puis fetch / Server Components à la place de getServerSideProps / getStaticProps.
  4. generateStaticParams pour les anciens getStaticPaths.
  5. 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. 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. 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. 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. 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. 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. 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 ?
Dès que le projet dépasse environ 50 dossiers, ou quand des zones fonctionnelles ont besoin de layouts racine différents. Idéal en multi-équipes : moins de conflits Git et une arborescence plus lisible.
Les groupes de routes modifient-ils l’URL ?
Non. Un dossier (marketing) est ignoré par Next.js. (marketing)/about/page.js reste /about, pas /marketing/about.
Les layouts imbriqués dégradent-ils les performances ?
Non, souvent l’inverse. Au changement de sous-route, les layouts parents ne se re-rendent pas ; seule la page la plus interne se recharge. L’état client dans un layout (ex. scroll de la sidebar) est conservé.
Différence entre routes parallèles et composants classiques ?
Chaque slot est un fragment de page indépendant avec son loading.js et error.js, chargement en parallèle. Un composant monolithique attend toutes les données ; une erreur peut bloquer toute la page.
Comment choisir la syntaxe d’interception ?
(.) pour le même niveau d’URL (ex. @modal et photos sous app), (..) pour le parent, (...) depuis la racine. En cas de doute, tester (...) puis affiner selon l’arborescence réelle des URL.
Comment éviter les conflits de routes ?
Un groupe par fonction ; une URL unique par chemin. Si deux pages doivent coexister, ajouter un segment réel (sans parenthèses) ou renommer l’une des routes.
Combien de temps pour refactorer un grand projet ?
Selon la taille : 1 à 2 semaines pour 50–100 pages, jusqu’à un mois pour les très grands sites. Commencer par un module pilote, puis étendre par migration incrémentale.

9 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog