Retour au cours

frontend / nextjs

Middleware

Leçon 91 exercice

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.ts avec un matcher restreint 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é.

ActionFonctionEffet visible pour l'utilisateur
RedirectionNextResponse.redirect(url)L'URL dans la barre d'adresse change
RéécritureNextResponse.rewrite(url)L'URL affichée ne change PAS
ContinuerNextResponse.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) et rewrite (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 Map en 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.

ts
// 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).*)",
  ],
};
ts
// 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*"],
};
ts
// 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;
}
ts
// 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...).
  • matcher limite le champ d'exécution — critique pour la performance globale.
  • NextResponse.redirect change l'URL visible, NextResponse.rewrite la garde inchangée.
  • Pour du rate limiting robuste en prod, préférer un store partagé (Redis) à une Map en mémoire (non partagée entre instances).

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →