Routes dynamiques Next.js et paramètres : guide complet de l'initiation au typage

La semaine dernière, en refactorant un projet Next.js, j’ai eu un problème énervant : la route dynamique suivait la doc, mais un clic menait à une 404. Console silencieuse, aucune erreur. J’ai fini par comprendre que Next.js 14 App Router a changé la façon de récupérer les paramètres — j’utilisais encore l’ancienne syntaxe Pages Router.
Ce n’est pas la première fois que je trébuche sur le routage Next.js. De getStaticPaths (Pages Router) à generateStaticParams (App Router), chaque upgrade oblige à réapprendre. Quand utiliser une route dynamique, un catch-all, des paramètres optionnels ? Tout se mélange facilement.
Si vous êtes dans la même situation — perdu avec les routes dynamiques Next.js ou en migration Pages → App Router — cet article est pour vous. Des bases aux pratiques de typage sûr, avec beaucoup d’exemples concrets.
À la fin : une vision complète des routes dynamiques, le bon type selon le scénario, la récupération correcte des paramètres et TypeScript pour des params typés. Pas de blabla : du code et des solutions. C’est parti.
Chapitre 1 : Bases des routes dynamiques
Qu’est-ce qu’une route dynamique ?
Scénario courant : un blog où chaque article a l’URL /blog/ID. Avec des routes statiques, il faudrait un fichier par article — impossible. Entrent les routes dynamiques : un seul fichier pour tous les détails.
Dans Next.js App Router, on utilise des dossiers entre crochets :
app/
├── blog/
│ └── [slug]/
│ └── page.tsx ← route dynamique
Cette structure correspond à tout /blog/* :
/blog/hello-world→slug = "hello-world"/blog/nextjs-guide→slug = "nextjs-guide"/blog/123→slug = "123"
Implémentation minimale
Créez app/blog/[slug]/page.tsx :
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return (
<div>
<h1>Article</h1>
<p>Slug actuel : {params.slug}</p>
</div>
)
}
Sur /blog/hello-world, params.slug vaut "hello-world".
Erreurs fréquentes des débutants :
- ❌ Fichier
[slug].tsx(App Router exige un dossier) - ❌ Accéder à
props.slug(passer parparams) - ❌ Oublier les crochets (sans crochets = route statique)
Pages Router vs App Router
| Caractéristique | Pages Router | App Router |
|---|---|---|
| Emplacement | pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
| Paramètres | router.query.slug ou getStaticProps | params.slug |
| Types | manuels | via props |
| Génération statique | getStaticPaths | generateStaticParams |
En migration, le plus déroutant est la récupération des paramètres. Pages Router : hook useRouter. App Router Server Components : pas de hooks, uniquement la prop params — rendu serveur par défaut, pas d’objet router côté client.
Cas pratique : fiche produit e-commerce
URL /products/ID :
// app/products/[id]/page.tsx
interface Product {
id: string
name: string
price: number
description: string
}
async function getProduct(id: string): Promise<Product | null> {
const products: Product[] = [
{ id: '1', name: 'Livre TypeScript', price: 99, description: 'Pour débutants' },
{ id: '2', name: 'Guide React', price: 129, description: 'De zéro à la prod' }
]
return products.find(p => p.id === id) || null
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
return <div>Produit introuvable</div>
}
return (
<div>
<h1>{product.name}</h1>
<p className="price">¥{product.price}</p>
<p>{product.description}</p>
</div>
)
}
Détails importants :
- Composant
async(Server Components) - Données d’abord, puis rendu
- Cas produit absent (404)
Pour une vraie page 404, utilisez notFound :
import { notFound } from 'next/navigation'
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
notFound()
}
return (
<div>
<h1>{product.name}</h1>
{/* ... */}
</div>
)
}
L’utilisateur voit votre not-found.tsx personnalisé. Vous maîtrisez les bases ; passons aux chemins multi-niveaux.
Chapitre 2 : Catch-All et paramètres optionnels
Quand utiliser un catch-all ?
Site de documentation :
/docs/getting-started/docs/api/authentication/docs/api/database/queries/docs/guides/deployment/vercel
Profondeur variable — une route dynamique simple ne suffit pas. Il faut un catch-all.
Catch-All : [...slug]
Nom de dossier [...slug] (trois points), profondeur quelconque :
app/
├── docs/
│ └── [...slug]/
│ └── page.tsx
Correspondances :
/docs/getting-started→slug = ["getting-started"]/docs/api/authentication→slug = ["api", "authentication"]/docs/guides/deployment/vercel→slug = ["guides", "deployment", "vercel"]
Attention : slug est un tableau, pas une chaîne.
Implémentation : système de docs
// app/docs/[...slug]/page.tsx
interface Doc {
title: string
content: string
}
async function getDoc(slugArray: string[]): Promise<Doc | null> {
const path = slugArray.join('/')
const docs: Record<string, Doc> = {
'getting-started': {
title: 'Démarrage rapide',
content: 'Bienvenue...'
},
'api/authentication': {
title: 'Authentification API',
content: 'Nous utilisons JWT...'
},
'api/database/queries': {
title: 'Requêtes base de données',
content: 'Requêtes avec Prisma...'
}
}
return docs[path] || null
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
const doc = await getDoc(params.slug)
if (!doc) {
return <div>Document introuvable</div>
}
return (
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.content }} />
<nav>
<a href="/docs">Docs</a>
{params.slug.map((segment, i) => {
const href = `/docs/${params.slug.slice(0, i + 1).join('/')}`
return (
<span key={i}>
{' / '}
<a href={href}>{segment}</a>
</span>
)
})}
</nav>
</article>
)
}
Points forts :
slugArray.join('/')pour le chemin- Fil d’Ariane avec
slice - Type
params: { slug: string[] }
Catch-All optionnel : [[...slug]]
Pour matcher /docs et /docs/* :
app/
├── docs/
│ └── [[...slug]]/
│ └── page.tsx
Correspondances :
/docs→slug = undefined/docs/getting-started→slug = ["getting-started"]/docs/api/auth→slug = ["api", "auth"]
Gérer slug optionnel :
// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
params
}: {
params: { slug?: string[] }
}) {
if (!params.slug) {
return <div>Bienvenue au centre de documentation</div>
}
const doc = await getDoc(params.slug)
// ...
}
Pièges fréquents
Piège 1 : oublier que slug est un tableau
// ❌ Incorrect
<h1>Chemin : {params.slug}</h1>
// ✅ Correct
<h1>Chemin : {params.slug.join('/')}</h1>
Piège 2 : mauvaise structure en génération statique
// ❌ Incorrect
export function generateStaticParams() {
return [
{ slug: 'api/auth' }
]
}
// ✅ Correct
export function generateStaticParams() {
return [
{ slug: ['api', 'auth'] }
]
}
Piège 3 : confondre les trois types
| Type | Dossier | Correspondance | Type param |
|---|---|---|---|
| Dynamique | [slug] | /blog/123 | string |
| Catch-All | [...slug] | /docs/a/b/c (pas /docs) | string[] |
| Catch-All opt. | [[...slug]] | /docs et /docs/a/b/c | string[] | undefined |
J’ai mélangé les trois — routes instables jusqu’à correction du nommage.
Astuce : caractères spéciaux
Pour URL avec caractères spéciaux, encodez/décodez :
export default async function Page({
params
}: {
params: { slug: string[] }
}) {
const decodedSlug = params.slug.map(s => decodeURIComponent(s))
console.log(params.slug)
console.log(decodedSlug)
// ...
}
Vous gérez les chemins complexes. Reste : quand générer ces pages ? À la demande ou au build ? C’est le rôle de generateStaticParams.
Chapitre 3 : generateStaticParams en profondeur
Pourquoi generateStaticParams ?
100 articles sur /blog/[slug], sans optimisation : requête BDD, SSR, réponse — lent et coûteux. Next.js pré-rend au build via generateStaticParams.
Usage de base : blog statique
// app/blog/[slug]/page.tsx
interface Post {
slug: string
title: string
content: string
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: Post) => ({
slug: post.slug
}))
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Effet :
generateStaticParamsau build → tous les slugs- HTML statique par slug
- Réponse instantanée à la visite
Artefacts build :
.next/server/app/blog/
├── hello-world.html
├── nextjs-guide.html
└── typescript-tips.html
Quand l’utiliser ?
✅ Adapté :
- Articles, actualités (contenu stable)
- Fiches produit (< ~10 000)
- Documentation, aide
- Profils utilisateur (volume modéré)
❌ Non adapté :
- Recherche (combinaisons infinies)
- Données temps réel (bourse, scores)
- UGC massif
- Contenu selon session/login
Catch-All en statique
Pour [...slug], retourner des tableaux :
// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
const docPaths = [
['getting-started'],
['api', 'authentication'],
['api', 'database', 'queries'],
['guides', 'deployment', 'vercel']
]
return docPaths.map(slug => ({ slug }))
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
// ...
}
Format : { slug: ['api', 'auth'] }, pas une chaîne.
Multi-paramètres : /shop/[category]/[productId]
app/
├── shop/
│ └── [category]/
│ └── [productId]/
│ └── page.tsx
// app/shop/[category]/[productId]/page.tsx
export async function generateStaticParams() {
const products = [
{ category: 'electronics', productId: 'iphone-15' },
{ category: 'electronics', productId: 'macbook-pro' },
{ category: 'books', productId: 'clean-code' },
{ category: 'books', productId: 'refactoring' }
]
return products.map(p => ({
category: p.category,
productId: p.productId
}))
}
export default async function ProductPage({
params
}: {
params: { category: string; productId: string }
}) {
return (
<div>
<h1>Catégorie : {params.category}</h1>
<p>ID produit : {params.productId}</p>
</div>
)
}
Génération à la demande (fallback)
Contenu massif (100 000 articles) : pré-rendre seulement le populaire :
// app/blog/[slug]/page.tsx
export const dynamicParams = true
export async function generateStaticParams() {
const topPosts = await fetchTopPosts(100)
return topPosts.map(post => ({
slug: post.slug
}))
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await fetchPost(params.slug)
if (!post) {
notFound()
}
return <article>{/* ... */}</article>
}
Avec dynamicParams = true :
- Pré-rendu : réponse immédiate
- Non pré-rendu : généré à la 1re visite, puis cache
- Inexistant : 404
Questions fréquentes
Q1 : quand s’exécute generateStaticParams ?
Au build (npm run build) uniquement. En dev (npm run dev), effet limité — builder pour voir les fichiers statiques.
Q2 : données mises à jour ?
Contenu figé après build. Solutions :
- ISR (Incremental Static Regeneration)
dynamicParams = truerevalidate
export const revalidate = 60
export default async function Page() {
// ...
}
Q3 : build trop long ?
Plus de chemins = build plus long. Réduire le pré-rendu, build incrémental (Vercel/Netlify), ou dynamicParams = true.
Dernière étape : typage TypeScript des paramètres de route.
Chapitre 4 : Typage sûr des paramètres
Pourquoi le typage ?
export default async function Page({
params
}: {
params: { slug: string }
}) {
const id = parseInt(params.slug)
if (isNaN(id)) {
return <div>ID invalide</div>
}
// ...
}
params.slug est string, besoin d’un nombre — erreur à l’exécution, pas à la compilation.
Contraintes de base
Par défaut, params est string ou string[]. Personnalisez :
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
const post = await fetchPost(params.slug)
// ...
}
Utile avec plusieurs paramètres :
// app/shop/[category]/[productId]/page.tsx
interface ShopParams {
category: 'electronics' | 'books' | 'clothing'
productId: string
}
export default async function ProductPage({
params
}: {
params: ShopParams
}) {
if (params.category === 'toys') { // ❌ erreur de compilation
// ...
}
}
Validation runtime avec Zod
Les types ne couvrent que la compilation. Zod pour le runtime :
npm install zod
// app/products/[id]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
const paramsSchema = z.object({
id: z.string().regex(/^\d+$/, 'ID numérique requis')
})
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound()
}
const { id } = result.data
const product = await fetchProduct(parseInt(id))
// ...
}
Avantages : compile-time + runtime, 404 sur requêtes invalides.
generateStaticParams typé
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export async function generateStaticParams(): Promise<BlogParams[]> {
const posts = await fetchAllPosts()
return posts.map(post => ({
slug: post.slug
}))
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// ...
}
Cas pratique : blog multilingue /[locale]/blog/[slug]
// app/[locale]/blog/[slug]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
const locales = ['zh', 'en', 'ja'] as const
type Locale = typeof locales[number]
interface PageParams {
locale: Locale
slug: string
}
const paramsSchema = z.object({
locale: z.enum(locales),
slug: z.string().min(1)
})
export async function generateStaticParams(): Promise<PageParams[]> {
const posts = await fetchAllPosts()
return locales.flatMap(locale =>
posts.map(post => ({
locale,
slug: post.slug
}))
)
}
export default async function BlogPost({
params
}: {
params: PageParams
}) {
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound()
}
const { locale, slug } = result.data
const post = await fetchPost(slug, locale)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
Atouts :
Locale="zh" | "en" | "ja"generateStaticParams→PageParams[]- Validation Zod runtime
- Chaîne compile + runtime stricte
Dépannage types
Q1 : params est Promise<...> ?
Next.js 15+ : params asynchrone :
export default async function Page({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
// ...
}
Next.js 14 (sync) :
export default async function Page({
params
}: {
params: { slug: string }
}) {
// ...
}
Q2 : params en any ?
Vérifier : strict mode tsconfig.json, types Next.js, nom page.tsx.
Q3 : détails erreur Zod
const result = paramsSchema.safeParse(params)
if (!result.success) {
console.error('Validation échouée :', result.error.format())
notFound()
}
Checklist typage
- Type
paramssur chaque route dynamique - Retour
generateStaticParamsaligné avecparams - Zod sur routes sensibles
- Strict mode TypeScript
- Unions / littéraux pour paramètres complexes
Conclusion
Vous maîtrisez désormais les routes dynamiques Next.js :
✅ Dynamique simple : [slug], récupération via params
✅ Catch-All : [...slug], paramètres optionnels [[...slug]]
✅ generateStaticParams : quand, comment, génération à la demande
✅ Typage : contraintes compile-time + validation runtime
Vous distinguez App Router et Pages Router, savez quand pré-rendre ou générer à la demande.
Prochaines étapes
Pratique immédiate :
- Créer une route dynamique et tester
params - Essayer catch-all si chemins multi-niveaux
- Ajouter types TypeScript et validation Zod
Approfondir :
- Routes parallèles (
@folder) - Routes interceptées (
(.)folder) - Groupes de routes
(folder) - Middleware pour auth et redirections
Ressources :
Aide rapide :
| Problème | Vérifier | Solution |
|---|---|---|
| 404 sur route dynamique | Nom dossier, generateStaticParams | Crochets, config statique |
params en any | Config TS | Strict mode, types params |
| Build trop long | Nombre de chemins | Moins de pré-rendu, dynamicParams |
| Données figées | Cache | revalidate ou dynamicParams |
Le passage Pages → App Router fait mal, mais une fois l’esprit App Router acquis, tout devient plus clair. Les routes dynamiques sont la base — données, cache, middleware suivront plus facilement.
En cas de blocage : doc officielle Troubleshooting, Issues GitHub Next.js, Discord Next.js.
Ouvrez l’éditeur et construisez vos routes dynamiques ! 🚀
Configuration complète des routes dynamiques Next.js
Étapes complètes de la création de routes dynamiques aux pratiques de typage sûr
⏱️ Estimated time: 2 hr
- 1
Step 1: Créer le dossier de route dynamique
Choisir le type selon le besoin :
• Paramètre unique : app/posts/[id]/page.tsx
• Multi-paramètres : app/posts/[category]/[id]/page.tsx
• Catch-all : app/posts/[...slug]/page.tsx
• Catch-all optionnel : app/posts/[[...slug]]/page.tsx
Règles de nommage :
• [id] : paramètre obligatoire
• [...slug] : capture tous les segments
• [[...slug]] : capture optionnelle de tous les segments - 2
Step 2: Récupérer les paramètres de route
Dans page.tsx :
• App Router utilise l'objet params
• params est une Promise, il faut await
• Utiliser la déstructuration pour chaque paramètre
Exemple :
export default async function Page({ params }) {
const { id } = await params
return <div>Post {id}</div>
}
Attention : params doit être await, sinon erreur - 3
Step 3: Configurer le typage sûr
Définir les types TypeScript :
• Interface pour params
• Type Promise<{ params }>
• Type de retour de generateStaticParams
Exemple :
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
} - 4
Step 4: Implémenter la génération statique (optionnel)
Utiliser generateStaticParams :
• Retourner toutes les combinaisons possibles
• Fonction async pour récupérer les données
• Génère statiquement toutes les pages
Exemple :
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ id: post.id }))
}
Note : uniquement pour la génération statique, pas pour les routes dynamiques pures - 5
Step 5: Gérer les paramètres optionnels
Route catch-all optionnelle :
• Syntaxe [[...slug]]
• params.slug peut être undefined
• Vérifier l'existence du paramètre
Exemple :
export default async function Page({ params }) {
const { slug } = await params
if (!slug) {
return <div>All posts</div>
}
return <div>Category: {slug.join('/')}</div>
} - 6
Step 6: Tester et valider
Points de test :
• Toutes les routes répondent
• Paramètres récupérés correctement
• Infos de type correctes
• Génération statique OK
Checklist :
• Toutes les routes dynamiques accessibles
• Types params corrects
• generateStaticParams retourne les bonnes données
• Erreurs 404 gérées
FAQ
Comment récupérer les paramètres de route dynamique ?
Points clés :
• params est une Promise, il faut await
• Déstructurer pour chaque paramètre
• Définir les types
Exemple :
export default async function Page({ params }) {
const { id } = await params
return <div>{id}</div>
}
Pourquoi une route dynamique renvoie 404 ?
• Nom de dossier incorrect ( [id] et non {id} )
• Chemin non correspondant (URL vs arborescence)
• Données generateStaticParams incomplètes
• Fichier page.tsx manquant
Solutions :
• Vérifier le nommage des dossiers
• Confirmer que l'URL correspond à l'arborescence
• Contrôler le retour de generateStaticParams
Différence entre catch-all et catch-all optionnel ?
• Au moins un segment requis
• /posts/[...slug] correspond à /posts/a, pas à /posts
Catch-all optionnel [[...slug]] :
• 0 segment ou plus
• /posts/[[...slug]] correspond à /posts et /posts/a/b
Usage :
• catch-all : au moins un paramètre
• catch-all optionnel : paramètre facultatif
Comment typer sûrement une route dynamique ?
1) Interface pour params
2) Type Promise<{ params }>
3) Type de retour de generateStaticParams
Exemple :
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
}
Quand utiliser generateStaticParams ?
Adapté :
• Valeurs de paramètres connues
• Génération statique de toutes les pages
• Performance et SEO
Non adapté :
• Paramètres changeants
• Trop de valeurs à énumérer
• Données en temps réel
Note : génération statique uniquement
Comment migrer les routes dynamiques depuis Pages Router ?
• getStaticPaths → generateStaticParams
• context.params → params (avec await)
• Format { paths, fallback } → tableau
Migration :
1) Remplacer getStaticPaths par generateStaticParams
2) Adapter la récupération (await params)
3) Mettre à jour les types
4) Tester toutes les routes
Comment gérer les routes multi-paramètres ?
app/posts/[category]/[id]/page.tsx
Récupération :
export default async function Page({ params }) {
const { category, id } = await params
return <div>{category} - {id}</div>
}
generateStaticParams retourne toutes les combinaisons :
export async function generateStaticParams() {
return [
{ category: 'tech', id: '1' },
{ category: 'tech', id: '2' },
// ...
]
}
10 min de lecture · Publié le: 25 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
Next.js App Router en pratique : groupes de routes et layouts imbriqués pour les grands projets
Groupes de routes, layouts imbriqués, routes parallèles et routes interceptées : structure de répertoires claire, moins de conflits d’URL et meilleure collaboration sur les grands projets Next.js.
Partie 5 sur 51
Suivant
Pièges courants du Next.js App Router et solutions : 8 retours d'expérience pour éviter les faux pas
De la récupération de données à la gestion d'erreurs : 14 pièges fréquents du Next.js App Router et leurs solutions. Server Components, Client Components, cache, migration — retours terrain pour éviter 80 % des erreurs courantes.
Partie 7 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire