frontend / nextjs
Middleware
Explication
Ce que vous allez apprendre
- Comprendre à quel moment le middleware s'exécute, avant même le routage vers une page
- Écrire un
middleware.tsavec unmatcherrestreint pour préserver la performance - Rediriger un visiteur non authentifié avec
NextResponse.redirect - Servir un contenu différent sans changer l'URL visible avec
NextResponse.rewrite - Comprendre pourquoi le middleware tourne sur l'Edge Runtime et ce que ça implique
Dans quel contexte ?
Une application SaaS a une zone /dashboard réservée aux utilisateurs connectés. Sans protection en amont, n'importe qui pourrait accéder à /dashboard/factures en tapant directement l'URL, avant même que le composant de la page ait eu la chance de vérifier quoi que ce soit. Le middleware placé à la racine du projet intercepte cette requête AVANT le rendu de la page et redirige vers /login si aucune session valide n'est trouvée dans les cookies.
Une image pour commencer
Imagine un vigile posté à l'entrée d'un bâtiment, qui contrôle chaque visiteur avant qu'il n'atteigne le bureau qu'il cherche. Le middleware Next.js joue exactement ce rôle.
Concrètement, il s'exécute pour CHAQUE requête, avant même que le routage vers une page ou une API ne commence. C'est l'endroit idéal pour des vérifications transversales : authentification, redirections, tests A/B, rate limiting basique.
Il faut connaître une contrainte technique importante avant d'écrire du middleware. Il tourne toujours sur l'Edge Runtime (vu dans une leçon dédiée) : rapide, mais sans accès aux APIs Node.js complètes.
Pourquoi cette limitation existe ? Comme le middleware s'exécute sur CHAQUE requête, sa performance impacte directement toute l'application — il doit donc rester très léger.
Voyons maintenant les usages concrets, du plus simple au plus subtil. D'abord la redirection conditionnelle : vérifier qu'un cookie de session existe avant de laisser passer vers une zone protégée, sinon rediriger vers /login.
Ensuite, un mécanisme plus discret : la réécriture (rewrite). Elle sert un contenu différent SANS que l'URL visible dans la barre d'adresse ne change — utile pour de l'A/B testing ou de l'internationalisation.
Enfin, on peut aussi modifier la requête ou la réponse au passage. Ajouter un identifiant de requête, un cookie de tracking, un header personnalisé.
| Action | Fonction | Effet visible pour l'utilisateur |
|---|---|---|
| Redirection | NextResponse.redirect(url) | L'URL dans la barre d'adresse change |
| Réécriture | NextResponse.rewrite(url) | L'URL affichée ne change PAS |
| Continuer | NextResponse.next() | La requête suit son cours normal |
Prérequis
Cette leçon suppose que tu es à l'aise avec les cookies et la notion de session, même si l'authentification complète est traitée dans une leçon dédiée plus loin dans le cours.
Les pièges courants à connaître avant de pratiquer :
- Confondre
redirect(change l'URL visible) etrewrite(sert un autre contenu discrètement, URL inchangée) : une erreur ici casse la navigation ou l'historique du navigateur. - Ne pas définir de
matcher: le middleware s'exécuterait alors sur CHAQUE requête, y compris les fichiers statiques, dégradant les performances inutilement. - Utiliser une
Mapen mémoire pour du rate limiting en production : elle n'est pas partagée entre plusieurs instances du serveur, rendant la protection illusoire à l'échelle — un store partagé comme Redis est nécessaire.
Piège fréquent
Oublier le matcher dans export const config fait tourner le middleware sur CHAQUE requête, y compris les images, le CSS et les fichiers _next/static. Sur une application avec beaucoup de trafic, ça dégrade sensiblement les performances pour une vérification qui n'a de sens que sur les pages HTML.
Et la suite ? Cette leçon complète les précédentes : Route Handlers et Server Actions gèrent la logique métier, le middleware gère ce qui doit être vérifié AVANT d'y arriver.
Commandes & code
Middleware
Le middleware s'exécute AVANT qu'une requête n'atteigne une route, sur l'Edge Runtime.
// middleware.ts — à la racine du projet (ou dans src/)
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Ajout d'un header sur chaque réponse
response.headers.set("x-request-id", crypto.randomUUID());
return response;
}
// Le matcher restreint les routes concernées (perf : évite d'exécuter partout)
export const config = {
matcher: [
// exclut les assets statiques et l'API interne de Next
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};// Redirection conditionnelle — auth guard
import { NextRequest, NextResponse } from "next/server";
const PROTECTED_PREFIXES = ["/dashboard", "/settings"];
export function middleware(request: NextRequest) {
const isProtected = PROTECTED_PREFIXES.some((p) =>
request.nextUrl.pathname.startsWith(p)
);
if (!isProtected) return NextResponse.next();
const token = request.cookies.get("session")?.value;
if (!token) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("from", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};// Réécriture (rewrite) transparente — A/B testing par exemple
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const bucket = request.cookies.get("ab-bucket")?.value ?? (Math.random() < 0.5 ? "a" : "b");
const url = request.nextUrl.clone();
url.pathname = `/experiments/${bucket}${url.pathname}`;
const response = NextResponse.rewrite(url); // l'URL visible ne change PAS
response.cookies.set("ab-bucket", bucket, { maxAge: 60 * 60 * 24 * 30 });
return response;
}// Middleware avec réponse anticipée (rate limiting basique par IP)
import { NextRequest, NextResponse } from "next/server";
const requestCounts = new Map<string, { count: number; resetAt: number }>();
export function middleware(request: NextRequest) {
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
const now = Date.now();
const entry = requestCounts.get(ip);
if (!entry || now > entry.resetAt) {
requestCounts.set(ip, { count: 1, resetAt: now + 60_000 });
} else if (entry.count >= 100) {
return NextResponse.json({ error: "Trop de requêtes" }, { status: 429 });
} else {
entry.count++;
}
return NextResponse.next();
}Résumé
- Le middleware tourne sur l'Edge Runtime : pas d'accès Node.js complet (pas de
fs, drivers DB TCP...). matcherlimite le champ d'exécution — critique pour la performance globale.NextResponse.redirectchange l'URL visible,NextResponse.rewritela garde inchangée.- Pour du rate limiting robuste en prod, préférer un store partagé (Redis) à une
Mapen mémoire (non partagée entre instances).
Exercices pratiques
Mission : le rate limiting qui ne protège rien en production
Objectif : Corriger un middleware sans matcher et diagnostiquer pourquoi son rate limiting en mémoire est illusoire à l'échelle.
Contexte
Un middleware.ts sans export const config a été déployé : il tourne désormais sur CHAQUE requête, y compris les images et le CSS servis depuis _next/static, ce qui ralentit tout le site. Ce même middleware utilise une Map en mémoire pour limiter chaque IP à 100 requêtes par minute. En production, l'application tourne sur 4 instances derrière un load balancer, et l'équipe constate que des IPs dépassent largement les 100 requêtes/minute sans être bloquées.