Le Middleware Next.js est l'un de ces outils qu'on sous-estime souvent au départ, mais qui finit par devenir indispensable dès qu'une application prend de l'ampleur. Protéger des routes, rediriger des utilisateurs non authentifiés, adapter les réponses selon la géolocalisation ou le rôle de l'utilisateur — tout ça se gère proprement dans un seul fichier, avant même que la page soit rendue.
Dans cet article, on va explorer en profondeur comment fonctionne le Middleware dans l'App Router, avec des cas d'usage concrets et du code TypeScript prêt à l'emploi.
Qu'est-ce que le Middleware Next.js ?
Le Middleware s'exécute avant le traitement d'une requête sur le serveur — entre l'arrivée de la requête et la réponse renvoyée au client. Concrètement, il tourne sur le Edge Runtime de Vercel (ou sur votre infrastructure Node.js), ce qui lui confère une latence extrêmement faible.
Son rôle peut être varié :
- Vérifier un token d'authentification et rediriger si absent
- Réécrire dynamiquement des URLs (A/B testing, i18n, etc.)
- Ajouter des headers de sécurité ou de CORS
- Logger des informations de requêtes
- Gérer des feature flags
Le fichier se nomme middleware.ts et doit être placé à la racine du projet (au même niveau que app/ ou src/).
Structure de base du Middleware
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
Le config.matcher est crucial : il définit sur quelles routes le middleware s'applique. Sans lui, il s'exécuterait sur chaque requête — y compris les assets statiques, ce qu'on veut éviter.
Syntaxe du matcher
export const config = {
matcher: [
// Appliquer sur toutes les routes sauf les assets et les API
'/((?!_next/static|_next/image|favicon.ico|api/).*)',
// Ou cibler précisément
'/dashboard/:path*',
'/admin/:path*',
'/profile',
],
}
Cas d'usage 1 : Protection des routes authentifiées
Le cas le plus fréquent. On veut rediriger vers /login si l'utilisateur n'a pas de session valide.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PROTECTED_ROUTES = ['/dashboard', '/admin', '/profile']
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Vérifier si la route est protégée
const isProtectedRoute = PROTECTED_ROUTES.some((route) =>
pathname.startsWith(route)
)
if (!isProtectedRoute) {
return NextResponse.next()
}
// Récupérer le token depuis les cookies
const token = request.cookies.get('auth-token')?.value
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/auth).*)'],
}
Attention : le Middleware tourne sur l'Edge Runtime — vous ne pouvez pas y utiliser des librairies Node.js classiques ni faire des appels à votre base de données directement. Vérifiez plutôt la présence et la validité basique du token (signature JWT par exemple).
Cas d'usage 2 : Vérification JWT sans base de données
Pour valider un JWT sans appel externe, utilisez la librairie jose qui est compatible Edge :
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET)
async function verifyToken(token: string): Promise<boolean> {
try {
await jwtVerify(token, SECRET)
return true
} catch {
return false
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (!pathname.startsWith('/dashboard')) {
return NextResponse.next()
}
const token = request.cookies.get('auth-token')?.value
if (!token || !(await verifyToken(token))) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
Cas d'usage 3 : Réécriture d'URLs (URL Rewriting)
Le rewriting permet de modifier l'URL traitée en interne sans changer l'URL visible dans le navigateur — idéal pour l'A/B testing ou les redirections de domaines.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Rediriger l'ancien blog vers les nouveaux slugs
if (pathname.startsWith('/blog/old-')) {
const newSlug = pathname.replace('/blog/old-', '/blog/')
return NextResponse.redirect(new URL(newSlug, request.url), { status: 301 })
}
// A/B testing : router 50% du trafic vers une variante
if (pathname === '/landing') {
const bucket = Math.random() < 0.5 ? 'a' : 'b'
return NextResponse.rewrite(new URL(`/landing-${bucket}`, request.url))
}
return NextResponse.next()
}
Différence entre redirect et rewrite
| Méthode | URL visible | Status HTTP | Cas d'usage |
|---|---|---|---|
NextResponse.redirect() | Change | 307/301 | Redirection permanente ou temporaire |
NextResponse.rewrite() | Ne change pas | 200 | A/B testing, routing interne |
Cas d'usage 4 : Ajout d'headers personnalisés
Utile pour passer des données à vos Server Components via les headers, ou pour ajouter des headers de sécurité globaux.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
// Passer l'URL courante aux Server Components
requestHeaders.set('x-current-path', request.nextUrl.pathname)
// Ajouter des headers de sécurité
const response = NextResponse.next({
request: { headers: requestHeaders },
})
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
)
return response
}
Vous pouvez ensuite lire ce header dans n'importe quel Server Component :
// app/dashboard/page.tsx
import { headers } from 'next/headers'
export default async function DashboardPage() {
const headersList = await headers()
const currentPath = headersList.get('x-current-path')
return <div>Vous êtes sur : {currentPath}</div>
}
Bonnes pratiques et pièges à éviter
✅ Ce qu'il faut faire
- Garder le middleware léger : il s'exécute sur chaque requête matchée — évitez les opérations lourdes
- Utiliser des cookies HttpOnly pour stocker les tokens d'auth
- Tester le matcher soigneusement : un matcher trop large peut ralentir votre app inutilement
- Gérer les erreurs avec un try/catch si vous faites des opérations async
❌ Ce qu'il faut éviter
- Ne pas appeler votre base de données depuis le middleware (pas de Prisma, pas de Mongoose) — l'Edge Runtime ne le supporte pas
- Ne pas importer de grosses librairies incompatibles avec l'Edge Runtime
- Ne pas oublier d'exclure
/_next/et/favicon.icodu matcher
Vérifier la compatibilité Edge
// Vérifiez la compatibilité des modules avec l'Edge Runtime
// en consultant : https://edge-runtime.vercel.app/features/available-apis
// ✅ Compatible Edge
import { jwtVerify } from 'jose'
// ❌ Non compatible Edge
import { verify } from 'jsonwebtoken' // dépend de Node.js crypto
Combiner avec les layouts et les Server Components
Une architecture solide combine le middleware (vérification rapide) avec la logique de session dans les Server Components (vérification complète) :
// app/dashboard/layout.tsx
import { redirect } from 'next/navigation'
import { getServerSession } from '@/lib/auth' // votre fonction de session
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await getServerSession()
if (!session) {
redirect('/login')
}
return (
<div>
<nav>{/* Navigation avec infos utilisateur */}</nav>
<main>{children}</main>
</div>
)
}
Le middleware fait office de première ligne de défense (rapide, Edge), et le layout assure la vérification complète avec accès à la base de données si nécessaire.
Conclusion
Le Middleware Next.js est un outil puissant, mais il faut comprendre ses contraintes (Edge Runtime, pas d'accès BDD direct) pour en tirer le meilleur parti. Utilisé correctement, il permet de centraliser toute la logique de protection des routes et de manipulation des requêtes en un seul endroit, avant même que vos composants soient rendus.
Pour aller plus loin, combinez-le avec les Server Actions vues dans un précédent article sur les Server Actions Next.js 15 pour avoir une stack d'authentification robuste et cohérente.
Vous travaillez sur un projet Next.js et vous souhaitez mettre en place une authentification solide ou une architecture frontend bien structurée ? N'hésitez pas à me contacter, je serai ravi d'en discuter avec vous.