Développement Web

App Router de Next.js 15 : routing par conventions de fichiers

11 min de lecture

L’App Router est la grande rupture conceptuelle introduite par Next.js 13 puis stabilisée et étendue dans la branche 15. Au lieu de définir chaque route en JSX ou via une configuration centralisée, on déclare l’arborescence de l’application directement par la structure de fichiers du dossier app/. Un nom de dossier devient un segment d’URL. Un fichier au nom conventionnel (page.tsx, layout.tsx, loading.tsx) devient un comportement spécifique pour ce segment. Le résultat est un système puissant qui rend le code pratiquement auto-documenté — il suffit de regarder l’arborescence pour comprendre la carte de l’application.

Ce tutoriel construit, étape par étape, l’arborescence d’une application e-commerce de petite taille avec accueil, catalogue produits, fiche produit, panier, espace compte protégé, et une page admin parallèle. À la fin, vous saurez utiliser les conventions essentielles, les segments dynamiques, les route groups, les routes parallèles et interceptantes, ainsi que la nouvelle Metadata API pour le SEO.

Prérequis

  • Node.js 20 LTS ou plus récent
  • Un projet Next.js 15 fraîchement initialisé avec App Router
  • Comprendre la frontière Server / Client Components (sinon, lire le tutoriel correspondant avant)
  • Niveau : intermédiaire
  • Temps estimé : 75 à 100 minutes

Si vous partez de zéro, créez le projet avec la commande standard avant de commencer.

pnpm create next-app@latest boutique --typescript --tailwind --app --turbopack --src-dir --no-linter

Vous obtenez un squelette avec un layout racine, une page d’accueil, et un fichier CSS global. C’est le point de départ.

Étape 1 — Comprendre les sept noms de fichiers spéciaux

Dans n’importe quel sous-dossier de app/, sept noms de fichiers ont une signification particulière. Les autres fichiers sont simplement ignorés par le routeur et peuvent contenir des composants utilitaires.

Fichier Rôle
page.tsx Définit l’UI rendue à l’URL du segment. Sans ce fichier, le segment n’est pas accessible.
layout.tsx Gabarit persistant qui enveloppe les pages enfants. Conservé en mémoire entre les navigations.
loading.tsx UI affichée pendant le chargement du segment (Suspense automatique).
error.tsx UI affichée si une erreur est lancée dans le sous-arbre (Error Boundary).
not-found.tsx UI affichée si notFound() est appelé dans le segment.
template.tsx Comme layout.tsx mais re-monté à chaque navigation (perd son état).
route.ts Définit un Route Handler HTTP (équivalent d’une API route). Ne peut pas cohabiter avec page.tsx dans le même dossier.

La règle de précédence est intuitive : un error.tsx au niveau d’un sous-dossier attrape les erreurs de ce sous-arbre, puis remonte au parent s’il n’existe pas. Idem pour loading.tsx et not-found.tsx. Cette hiérarchie permet d’avoir un fallback global au niveau racine et des comportements spécialisés sur certaines sections.

Étape 2 — Le layout racine et les conventions de base

Le app/layout.tsx est obligatoire. C’est le seul layout qui doit contenir les balises <html> et <body>. Tous les layouts descendants se contentent d’envelopper leurs children.

// src/app/layout.tsx
import './globals.css';
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: { default: 'Boutique Aria', template: '%s | Boutique Aria' },
  description: 'Casques et accessoires audio livres en 48 heures.',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr">
      <body className="min-h-screen bg-white text-gray-900">
        <header className="border-b p-4">
          <a href="/" className="text-xl font-bold">Boutique Aria</a>
        </header>
        <main className="max-w-5xl mx-auto p-6">{children}</main>
      </body>
    </html>
  );
}

Notez l’objet metadata exporté : c’est l’API officielle Next.js 15 pour gérer les balises <title>, <meta>, OpenGraph et Twitter Card. Le template permet d’afficher Catalogue | Boutique Aria sur les pages enfants qui ne définissent que leur propre titre. Cela remplace complètement next/head du Pages Router.

Ajoutons maintenant une page d’accueil au niveau du segment racine.

// src/app/page.tsx
export default function HomePage() {
  return (
    <section>
      <h1 className="text-3xl font-bold">Nos casques</h1>
      <p className="mt-2 text-gray-600">Selection de modeles testes par notre equipe.</p>
    </section>
  );
}

Lancez pnpm dev et visitez http://localhost:3000. Vous voyez le header (rendu par le layout) et la section (rendue par la page). Le layout est persistant — quand on naviguera vers d’autres pages, le header restera en mémoire sans re-rendu, ce qui rend les transitions instantanées.

Étape 3 — Segments dynamiques et catch-all

Les segments dynamiques utilisent les crochets dans le nom de dossier. Pour un catalogue où chaque produit a une URL en /produits/abc-123, on crée app/produits/[slug]/page.tsx.

// src/app/produits/[slug]/page.tsx
export default async function FicheProduit({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>Produit : {slug}</h1>;
}

Détail important pour Next.js 15 : params est une Promise, pas un objet direct. Il faut donc await params avant de lire ses propriétés. C’est un changement breaking par rapport à la 14 ; le codemod npx @next/codemod@latest upgrade migre automatiquement la majorité des cas.

Pour matcher plusieurs segments d’un coup, on utilise la syntaxe catch-all : app/docs/[...slug]/page.tsx capture /docs/intro mais aussi /docs/api/v2/auth en exposant slug comme un tableau ['api', 'v2', 'auth']. La variante [[...slug]] rend le paramètre optionnel et matche aussi /docs tout court.

// src/app/docs/[...slug]/page.tsx
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params;
  return <h1>Chemin : {slug.join(' / ')}</h1>;
}

Pour générer statiquement plusieurs valeurs d’un segment dynamique au build, exporter une fonction generateStaticParams dans le même fichier. Next.js l’appelle pour récupérer la liste des slugs à pré-rendre.

Étape 4 — Loading et Error UI

L’un des grands avantages de l’App Router est l’intégration native de Streaming et Error Boundaries. Pour activer un état de chargement automatique sur une route, il suffit d’ajouter un fichier loading.tsx à côté du page.tsx.

// src/app/produits/[slug]/loading.tsx
export default function LoadingProduit() {
  return (
    <div className="animate-pulse">
      <div className="h-8 w-2/3 bg-gray-200 rounded"></div>
      <div className="h-4 w-1/3 bg-gray-200 rounded mt-3"></div>
    </div>
  );
}

Next.js enveloppe automatiquement le page.tsx dans une <Suspense> et utilise ce loading.tsx comme fallback. Tant que le await getProduit(slug) dans la page n’est pas résolu, le skeleton s’affiche. Aucune logique d’état de chargement à écrire à la main.

Pour les erreurs, on ajoute un error.tsx. Ce composant doit obligatoirement être un Client Component, car il reçoit la prop reset qui est une fonction.

// src/app/produits/[slug]/error.tsx
'use client';

export default function ErreurProduit({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="p-6 border border-red-200 rounded bg-red-50">
      <h2 className="font-bold text-red-800">Erreur de chargement</h2>
      <p className="text-sm text-red-700 mt-2">{error.message}</p>
      <button onClick={reset}
        className="mt-4 px-3 py-1 border border-red-300 rounded">
        Reessayer
      </button>
    </div>
  );
}

L’Error Boundary attrape toute exception lancée par les composants serveur ou client du sous-arbre. Le bouton Reessayer appelle reset() qui re-tente le rendu du segment. Pour les erreurs vraiment fatales (qui surviennent dans le layout racine lui-même), il faut un app/global-error.tsx qui doit aussi inclure ses propres <html> et <body>.

Étape 5 — Route groups et organisation

Quand l’arborescence grossit, on a besoin de regrouper des routes sans les faire apparaître dans l’URL. La syntaxe (nom) avec parenthèses crée un route group : un dossier visible dans le code, invisible dans l’URL.

Exemple : on veut séparer les pages marketing (accueil, à propos, contact) de l’application (catalogue, panier, compte) parce qu’elles ont des layouts différents. On crée deux route groups.

src/app/
├── (marketing)/
│   ├── layout.tsx       // header simple, footer marketing
│   ├── page.tsx          // /
│   ├── about/
│   │   └── page.tsx      // /about
│   └── contact/
│       └── page.tsx      // /contact
├── (boutique)/
│   ├── layout.tsx        // header avec panier, sidebar filtres
│   ├── produits/
│   │   ├── page.tsx      // /produits
│   │   └── [slug]/
│   │       └── page.tsx  // /produits/[slug]
│   └── panier/
│       └── page.tsx      // /panier
└── layout.tsx            // layout racine commun

Les URLs ne contiennent ni (marketing) ni (boutique) — uniquement les segments réels. Mais chaque groupe peut avoir son propre layout.tsx qui s’applique à toutes ses routes. C’est extrêmement utile pour séparer un espace public d’un espace authentifié, sans dupliquer la structure d’URL.

Étape 6 — Routes parallèles et interceptantes

Deux fonctionnalités avancées de l’App Router ouvrent des UX impossibles à faire en routing classique : les parallel routes et les intercepting routes. Elles se combinent souvent pour créer des modales partageables par URL.

Une parallel route est définie par un dossier préfixé d’un arobase, par exemple @modal. Elle se rend en parallèle de la page principale, et le layout reçoit deux props : children (la page normale) et modal (la parallel route).

// src/app/(boutique)/layout.tsx
export default function BoutiqueLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <>
      {children}
      {modal}
    </>
  );
}

Couplée à une intercepting route (préfixe (.), (..) ou (...)), on peut intercepter une navigation et afficher une modale au lieu de quitter la page courante. Cas d’usage classique : sur une grille de produits, cliquer sur un produit ouvre une modale, mais l’URL devient /produits/abc ; un rafraîchissement de la page affiche la fiche en plein écran.

src/app/(boutique)/
├── @modal/
│   ├── default.tsx                    // rendu par défaut (null si pas de modale)
│   └── (.)produits/[slug]/page.tsx    // intercepte /produits/[slug]
├── produits/
│   ├── page.tsx
│   └── [slug]/
│       └── page.tsx                   // fiche produit plein écran
└── layout.tsx

Le fichier default.tsx est obligatoire dans chaque parallel route pour gérer le cas « pas de contenu actif ». Sans lui, un rafraîchissement direct casse le rendu.

Étape 7 — Middleware et protection de routes

Pour protéger un groupe de routes derrière une authentification, on utilise un middleware.ts à la racine du projet (à côté du dossier app/). Il s’exécute avant chaque requête et peut rediriger, réécrire, ou laisser passer.

// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;
  const isProtected = request.nextUrl.pathname.startsWith('/compte');

  if (isProtected && !token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/compte/:path*', '/admin/:path*'],
};

Le matcher évite que le middleware ne tourne pour toutes les requêtes (assets statiques inclus), ce qui économise du temps CPU sur les fonctions Edge. Depuis Next.js 15.5, le runtime Node.js est stable pour les middlewares (il était experimental à partir de 15.2) — utile si on a besoin d’appeler une base via TCP plutôt que les seules APIs Edge-compatibles.

Erreurs fréquentes

Erreur Cause Solution
« params should be awaited before using its properties » Code écrit pour Next.js 14 où params était sync Ajouter await et marquer la fonction async ; exécuter npx @next/codemod@latest upgrade pour le faire automatiquement sur tout le projet
404 sur une page qui devrait exister Oubli du fichier page.tsx dans le segment Vérifier que le segment a bien un page.tsx (un dossier seul ne crée pas de route)
Le loading.tsx ne s’affiche jamais Le composant page n’a aucun await qui prend du temps, ou les données sont en cache Vérifier que la fonction de fetch n’est pas mise en cache de manière agressive ; ajouter un délai artificiel pour tester
« Each child in a list should have a unique key prop » sur une route parallèle Manque du fichier default.tsx dans une parallel route Créer le default.tsx qui retourne null par défaut
Middleware qui tourne pour toutes les requêtes y compris les images Pas de matcher exporté Définir un config.matcher précis pour limiter les routes concernées
Layout qui se re-monte à chaque navigation au lieu de persister Utilisation accidentelle de template.tsx au lieu de layout.tsx Renommer en layout.tsx sauf si le re-montage est volontaire (animations d’entrée par exemple)

Pour aller plus loin

Cet article fait partie d’une série sur React 19 et Next.js 15. Pour la vue d’ensemble et le parcours conseillé, consultez React 19 + Next.js 15 : architecture full-stack moderne.

Tutoriels frères qui s’appuient sur les conventions vues ici :

Ressources officielles :

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité