App Router dans Next.js : Layout, Page et Route
1. Aperçu
App Router est le mécanisme de routage par défaut de Next.js depuis la version 13, remplaçant progressivement Pages Router (pages/). Il est basé sur la convention de fichiers dans le répertoire app/ : page.tsx crée une route, layout.tsx crée une UI commune, route.ts crée un endpoint API. Après avoir lu cet article, vous saurez comment organiser un projet Next.js avec App Router, écrire des layouts imbriqués, créer des routes dynamiques et des route handlers, ainsi que comment tester ces composants.
2. Structure de répertoire de base
App Router utilise folder = segment URL. Chaque dossier dans app/ peut contenir des fichiers spéciaux qui définissent le comportement de la route.
app/
├── layout.tsx # layout racine, doit contenir <html>, <body>
├── page.tsx # route "/"
├── about/
│ └── page.tsx # route "/about"
├── products/
│ ├── layout.tsx # layout spécifique pour /products/*
│ ├── page.tsx # route "/products"
│ └── [id]/
│ └── page.tsx # route "/products/123"
└── api/
└── orders/
└── route.ts # API "/api/orders"
layout.tsxenveloppe chaquepage.tsxau même niveau ou plus profond.page.tsxest le point de terminaison qui rend réellement le contenu pour une URL.route.tsne rend pas d’UI, il retourne uneResponsecomme un endpoint REST.
Remarque : Les noms de dossier ne commençant pas par une lettre minuscule (par exemple
Products) fonctionnent toujours, mais Next.js recommande d’utiliser kebab-case ou camelCase de façon cohérente pour éviter les confusions sur les systèmes d’exploitation sensibles à la casse.
3. Page — définir une route
Le fichier page.tsx exporte un composant React qui constitue l’UI principale pour la route. Par défaut, c’est un Server Component.
// app/products/page.tsx
import { ProductService } from "@/services/product-service";
export default async function ProductsPage() {
const products = await ProductService.findAll();
return (
<main>
<h1>Liste des produits</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name} - {product.price}đ</li>
))}
</ul>
</main>
);
}
async function ProductsPagepermetawaitde données directement dans le composant car c’est un Server Component, sans besoin deuseEffectougetServerSideProps.- Next.js rend automatiquement le HTML côté serveur et l’envoie au navigateur avec le contenu déjà prêt — bon pour le SEO et le Time to First Contentful Paint.
- La route correspond à l’URL
/products.
3.1. Route dynamique avec les paramètres
// app/products/[id]/page.tsx
import { ProductService } from "@/services/product-service";
import { notFound } from "next/navigation";
type ProductPageProps = {
params: { id: string };
};
export default async function ProductDetailPage({ params }: ProductPageProps) {
const product = await ProductService.findById(params.id);
if (!product) {
notFound();
}
return <h1>{product.name}</h1>;
}
- Le dossier
[id]crée un segment dynamique, Next.js injecte la valeur réelle dansparams.id. notFound()rend le fichiernot-found.tsxle plus proche au lieu de faire crasher l’app — c’est la façon standard de gérer les erreurs 404 dans App Router.
4. Layout — UI commune
Layout reçoit la prop children qui est la route enfant, conserve l’état lors de la navigation entre les pages du même layout (par exemple, la sidebar ne re-rend pas).
// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="products-layout">
<aside>Filtre des produits</aside>
<section>{children}</section>
</div>
);
}
childrenici est le contenu deapp/products/page.tsxouapp/products/[id]/page.tsx.- Lors de la navigation de
/productsà/products/123, la partie<aside>n’est pas démontée/remontée — c’est le principal avantage du layout par rapport à l’importation manuelle d’un composant commun dans chaque page.
4.1. Root layout est obligatoire
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Shop Nón Lá",
description: "Boutique de démonstration App Router",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr">
<body>
<header>Shop Nón Lá</header>
{children}
</body>
</html>
);
}
- Root layout doit contenir
<html>et<body>car c’est le layout le plus haut, qui enveloppe l’ensemble de l’application. export const metadataest la façon de déclarer<title>,<meta>de manière déclarative, Next.js l’injecte automatiquement dans<head>— pas besoin de manipulation manuelle du DOM.
Remarque : Si vous oubliez de créer
app/layout.tsx, Next.js signalera une erreur de build immédiatement car chaque route a besoin au minimum d’un root layout.
5. Route Groups et Parallel Segments
Parfois, vous voulez regrouper les routes pour organiser le code mais sans que ce groupe n’apparaisse dans l’URL. Utilisez route group avec une paire de parenthèses (name).
app/
├── (marketing)/
│ ├── layout.tsx # layout spécifique pour les pages marketing
│ ├── page.tsx # route "/"
│ └── pricing/
│ └── page.tsx # route "/pricing"
└── (dashboard)/
├── layout.tsx # layout spécifique pour le dashboard
└── settings/
└── page.tsx # route "/settings"
(marketing)et(dashboard)n’apparaissent pas dans l’URL, ils servent uniquement à appliquer des layouts différents pour chaque groupe de pages./pricinget/settingsutilisent des layouts différents bien qu’ils se trouvent tous deux sousapp/.
6. Route Handlers — construire une API dans App Router
Le fichier route.ts remplace pages/api/*.ts de Pages Router, exporte les fonctions selon le nom de la méthode HTTP.
// app/api/orders/route.ts
import { NextRequest, NextResponse } from "next/server";
import { OrderService } from "@/services/order-service";
export async function GET() {
const orders = await OrderService.findAll();
return NextResponse.json(orders);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const order = await OrderService.create(body);
return NextResponse.json(order, { status: 201 });
}
GETetPOSTsont deux exports séparés, Next.js route automatiquement la requête selon la méthode HTTP vers la bonne fonction.NextResponse.json(...)définitContent-Type: application/jsonautomatiquement, vous n’avez pas besoin deJSON.stringifymanuel.- Cet endpoint est servi à
/api/orders, complètement indépendant de l’UI — ne rend pas React.
6.1. Route handler dynamique
// app/api/orders/[id]/route.ts
import { NextResponse } from "next/server";
import { OrderService } from "@/services/order-service";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const order = await OrderService.findById(params.id);
if (!order) {
return NextResponse.json({ message: "Order not found" }, { status: 404 });
}
return NextResponse.json(order);
}
- Comme la page dynamique, le dossier
[id]permet de récupérer la valeur de l’URL viaparams.id. - Retourner
status: 404explicitement au lieu de laisser le client deviner l’erreur.
7. Fichiers spéciaux : loading, error, not-found
App Router utilise React Suspense et Error Boundary implicitement via des fichiers de convention, vous n’avez pas besoin d’écrire les boundaries vous-même.
// app/products/loading.tsx
export default function ProductsLoading() {
return <p>Chargement de la liste des produits...</p>;
}
// app/products/error.tsx
"use client";
export default function ProductsError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>Erreur : {error.message}</p>
<button onClick={reset}>Réessayer</button>
</div>
);
}
loading.tsxs’affiche automatiquement tandis quepage.tsxdu même segment attend les données — pas besoin deuseState(isLoading).error.tsxdoit être un Client Component ("use client") car il utilisereset— une fonction d’interaction côté client pour re-rendre le segment défaillant.- Next.js enveloppe automatiquement le segment avec
<Suspense>et le boundary d’erreur correspondant en fonction de ces fichiers.
8. Métadonnées et layouts imbriqués
Chaque segment peut exporter sa propre metadata, Next.js les fusionne dans l’ordre du layout racine à la page.
// app/products/[id]/page.tsx
import type { Metadata } from "next";
import { ProductService } from "@/services/product-service";
type Props = { params: { id: string } };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await ProductService.findById(params.id);
return { title: product ? `${product.name} - Shop Nón Lá` : "Non trouvé" };
}
export default async function ProductDetailPage({ params }: Props) {
const product = await ProductService.findById(params.id);
return <h1>{product?.name}</h1>;
}
generateMetadataest une fonction dynamique, elle permet de définir<title>en fonction des données réelles du produit — utile pour le SEO des pages détaillées.- Les métadonnées de la page enfant remplacent les champs du même nom du layout parent, les autres champs restent inchangés.
9. Tester les composants de page
Avec les Server Components asynchrones, la façon la plus simple de tester est d’appeler directement la fonction du composant et de vérifier le JSX retourné, ou d’utiliser React Testing Library après le rendu.
<!-- package.json (partie devDependencies, raccourcie) -->
<dependencies>
<vitest>^2.0.0</vitest>
<testing-library-react>^16.0.0</testing-library-react>
</dependencies>
// services/product-service.test.ts
import { describe, it, expect, vi } from "vitest";
import { ProductService } from "@/services/product-service";
import ProductsPage from "@/app/products/page";
import { render, screen } from "@testing-library/react";
vi.mock("@/services/product-service", () => ({
ProductService: {
findAll: vi.fn(),
},
});
describe("ProductsPage", () => {
it("affiche la liste des produits retournée par le service", async () => {
(ProductService.findAll as any).mockResolvedValue([
{ id: "1", name: "Nón lá A", price: 50000 },
]);
const jsx = await ProductsPage();
render(jsx);
expect(screen.getByText("Nón lá A - 50000đ")).toBeInTheDocument();
});
});
ProductsPage()est appelée comme une fonction async pure car c’est essentiellement une fonction retournant du JSX — pas besoin de framework de test spécial pour les Server Components.vi.mockremplaceProductService.findAllpar un stub retournant des données simulées, ce qui permet au test de ne pas dépendre de la vraie base de données.render(jsx)de Testing Library vérifie que le JSX rendu dans le DOM contient le contenu attendu.
10. Erreurs courantes et quand ne pas l’utiliser
- Oublier
"use client"quand utiliser des hooks commeuseState/useEffect: les Server Components ne supportent pas ces hooks, Next.js signalera une erreur de build claire. - Placer la logique de récupération de données sensibles (clé API) dans un Client Component : car le code sera regroupé dans le navigateur. Priorisez toujours la récupération de données dans un Server Component ou un Route Handler.
- Abuser des route groups juste pour “faire beau” : si vous n’avez pas vraiment besoin de layouts différents, les route groups ne font que compliquer l’arborescence.
- Mélanger
pages/etapp/pour la même route : les deux routeurs ne peuvent pas traiter la même URL, Next.js prioriseraapp/et les routes danspages/seront ignorées — cela peut prêter à confusion lors d’une migration progressive.
Remarque : Si votre projet fonctionne bien avec Pages Router et que vous n’avez pas besoin de Server Components, Streaming ou Server Actions, migrer immédiatement vers App Router n’est pas toujours nécessaire. Évaluez les avantages avant de changer tout le routage.
11. Conclusion
App Router organise l’application Next.js via trois types de fichiers principaux : page.tsx définit une route, layout.tsx partage l’UI entre plusieurs routes, et route.ts construit une API sans rendre d’HTML. Combiné avec les fichiers de convention comme loading.tsx, error.tsx, not-found.tsx, vous pouvez construire une expérience de chargement/erreur fluide sans avoir besoin d’écrire les boundaries vous-même. Lors de la conception de l’arborescence, priorisez le regroupement selon les layouts vraiment nécessaires à partager, évitez les route groups superflus et vérifiez toujours la frontière Server/Client Component pour ne pas fuir la logique sensible vers le navigateur.