Le Middleware Next.js est l'un des outils les plus puissants de l'App Router, et pourtant l'un des plus mal compris. Il s'exécute avant que la requête n'atteigne vos Server Components ou vos Route Handlers — à l'edge, dans un environnement V8 isolé sans accès à Node.js. Ce positionnement unique lui permet d'agir sur chaque requête avec une latence minimale, quel que soit le CDN utilisé.
Concrètement, le middleware peut réécrire des URLs, rediriger des utilisateurs, modifier des headers, lire et écrire des cookies. Il ne rend pas de HTML et ne remplace pas vos Server Components — son rôle est d'intercepter, filtrer et transformer la requête avant qu'elle n'arrive à votre application. Mal utilisé, il devient un goulot d'étranglement. Bien utilisé, il simplifie l'authentification, le rate limiting et la personnalisation sans toucher à un seul composant.
Cet article couvre les patterns concrets qui reviennent dans tout projet Next.js sérieux en 2026 : garde d'authentification, rate limiting avec Redis, redirections géographiques et injection de feature flags.
Comment fonctionne le Middleware Next.js à l'edge
Le fichier middleware.ts placé à la racine de votre projet (ou dans /src) est compilé en edge runtime — un environnement V8 isolé distinct de Node.js. Cela signifie pas de fs, pas de path, pas de modules natifs Node.js. La contrepartie : démarrage à froid quasi instantané (< 1 ms vs plusieurs dizaines de ms pour une Lambda) et déploiement mondial sur le CDN de Vercel ou Cloudflare.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// request.nextUrl contient l'URL parsée
// request.cookies donne accès aux cookies
// request.headers expose les headers entrants
return NextResponse.next() // laisser passer la requête
}
// Limiter l'exécution aux routes qui en ont besoin
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
Le matcher est crucial : sans lui, le middleware s'exécute sur chaque requête, y compris les assets statiques (/_next/static, /favicon.ico). Définir des patterns précis réduit les invocations inutiles et évite des comportements inattendus.
Protéger vos routes avec un middleware d'authentification
C'est le cas d'usage le plus courant. Plutôt que de dupliquer une vérification de session dans chaque layout ou Server Component, le middleware centralise la logique de protection.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from '@/lib/auth/edge' // doit être compatible edge runtime
const PROTECTED_ROUTES = ['/dashboard', '/settings', '/api/private']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const isProtected = PROTECTED_ROUTES.some(route => pathname.startsWith(route))
if (!isProtected) return NextResponse.next()
const token = request.cookies.get('auth-token')?.value
if (!token) {
// Préserver l'URL cible pour redirection post-login
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
try {
const payload = await verifyToken(token) // JWT verify compatible edge (jose, pas jsonwebtoken)
// Passer l'userId en header pour les Server Components
const response = NextResponse.next()
response.headers.set('x-user-id', payload.sub)
response.headers.set('x-user-role', payload.role)
return response
} catch {
// Token invalide ou expiré
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('auth-token')
return response
}
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/api/private/:path*'],
}
Point important : jsonwebtoken utilise des APIs crypto Node.js et n'est pas compatible edge. Utilisez jose qui fonctionne partout (Web Crypto API). Si vous utilisez Better Auth ou Clerk, ils fournissent leurs propres helpers middleware déjà optimisés pour l'edge.
Rate limiting avec Upstash Redis en edge
Le middleware est l'endroit idéal pour implémenter un rate limiting global — avant même que vos Route Handlers ou Server Actions ne soient invoqués. Upstash propose un client Redis HTTP compatible edge runtime.
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requêtes par 10 secondes
analytics: true,
})
export async function middleware(request: NextRequest) {
const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'anonymous'
const { success, limit, reset, remaining } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Trop de requêtes. Réessayez dans quelques secondes.' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
)
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
Upstash facture à l'invocation et non à la connexion persistante, ce qui le rend parfaitement adapté à l'architecture serverless/edge. Pour des projets à fort trafic, différenciez les fenêtres glissantes par route : plus restrictif sur /api/auth (5 req/10s), plus souple sur /api/public (50 req/10s).
Redirections géographiques et personnalisation par locale
Next.js expose geo et ip directement sur NextRequest en production (Vercel et Cloudflare). Pour des redirections simples, aucun appel externe n'est nécessaire.
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const country = request.geo?.country ?? 'FR'
if (!pathname.startsWith('/en') && !pathname.startsWith('/fr')) {
const locale = country === 'GB' || country === 'US' ? 'en' : 'fr'
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url))
}
return NextResponse.next()
}
Pour l'i18n dans Next.js App Router avec next-intl, le middleware est fourni par la librairie — inutile de le réimplémenter. Le middleware custom reste utile pour les règles métier que next-intl ne couvre pas : adapter l'expérience selon le domaine d'origine, ou forcer une variante locale selon des critères business spécifiques.
Injection de feature flags et A/B testing sans JavaScript côté client
Le middleware peut lire un cookie de variante et injecter un header que vos Server Components consomment via headers(). A/B testing sans re-render, sans hydratation — directement au niveau HTML.
export function middleware(request: NextRequest) {
const response = NextResponse.next()
let variant = request.cookies.get('ab-variant')?.value
if (!variant) {
variant = Math.random() < 0.5 ? 'A' : 'B'
response.cookies.set('ab-variant', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 jours
httpOnly: true,
})
}
response.headers.set('x-ab-variant', variant)
return response
}
Dans votre Server Component, lisez la variante via import { headers } from 'next/headers' puis (await headers()).get('x-ab-variant'). La variante est persistée par cookie, cohérente entre les sessions, et ne déclenche aucune hydratation côté client.
Les pièges à éviter absolument
Limite de taille du bundle : le middleware est limité à 1 MB sur Vercel, 2 MB self-hosted. Évitez les imports de librairies lourdes (lodash, ORM, modules Node.js en cascade). Vérifiez la taille compilée avec next build --debug.
Pas de logique métier lourde : vérifier un JWT à l'edge ? Oui. Interroger votre base de données pour vérifier les permissions ? Non — latence réseau et absence de pool de connexion rendent ça contre-productif. Injectez l'userId en header et laissez vos Server Components faire la vérification fine en base.
Évitez les boucles de redirection en excluant explicitement les pages publiques :
// ✅ Exclure /login et /register de la protection
const PUBLIC_ROUTES = ['/login', '/register', '/api/auth']
if (PUBLIC_ROUTES.some(route => pathname.startsWith(route))) {
return NextResponse.next()
}
Testez en local : request.geo et request.ip sont undefined en développement. Mockez ces valeurs dans vos tests d'intégration Playwright ou conditionnez leur usage avec un fallback.
En pratique
Le middleware d'authentification fonctionne en tandem avec votre solution d'auth. L'article sur Better Auth, Clerk et AuthJS détaille quelle librairie choisir selon votre stack et comment intégrer leur middleware prêt à l'emploi — évitant de réimplémenter la vérification JWT à la main.
Pour le rate limiting en production, coupler le middleware Upstash avec l'optimisation des requêtes Prisma constitue une défense en profondeur : le middleware bloque les abus en amont, la couche data reste saine en aval. Si vous construisez des APIs avec des Route Handlers Next.js, le middleware est leur compagnon naturel pour l'authentification avant que la requête n'y arrive.
Sources
- Next.js Middleware — documentation officielle — Next.js docs
- Upstash Ratelimit SDK — rate limiting edge-compatible
- jose — JavaScript implementation of JWTs — JWT pour edge runtime (Web Crypto API)