Cours Next.js · Module 2: Routing et layouts
Middleware et interception des requêtes
Dans cette leçon15
Au cœur de Metropolis Quantum se trouve un système avancé de contrôle d'accès et de redirection appelé la Quantum Gateway. Ce système surveille chaque tentative d'accès aux différentes zones de la ville, vérifie les autorisations, redirige vers les points appropriés et modifie les itinéraires de voyage en temps réel. C'est un élément d'infrastructure extrêmement important qui garantit à la fois la sécurité et l'optimisation du trafic dans toute la ville.
Dans le monde de Next.js 16, le Proxy (appelé Middleware jusqu'à la version 15) joue un rôle similaire - un mécanisme puissant qui permet d'intercepter et de modifier les requêtes HTTP avant qu'elles ne soient traitées par une page ou une API, permettant d'implémenter des fonctionnalités comme l'autorisation, les redirections ou la modification des réponses.
Qu'est-ce que le Proxy (anciennement Middleware) dans Next.js 16 ?
Le proxy est une fonction qui s'exécute avant qu'une requête ne soit traitée par une page ou une route d'API. Il opère entre la requête du client et la réponse du serveur, offrant la possibilité de :
- Exécuter du code avant le chargement de la page
- Modifier l'objet de la requête
- Modifier l'objet de la réponse
- Rediriger l'utilisateur
- Rejeter les requêtes
- Ajouter des en-têtes HTTP
- Modifier les cookies
Structure de base d'un middleware
Dans Next.js 16, tu places ce code dans un fichier proxy.ts (ou proxy.js) à la racine du projet (à côté de app ou pages, ou dans src si ton projet l'utilise). Jusqu'à la version 15, le fichier s'appelait middleware.ts et la fonction exportée middleware : Next.js 16 a renommé les deux, mais la logique n'a pas changé. L'ancien fichier fonctionne encore, mais il est déprécié, et le proxy s'exécute toujours dans l'environnement Node.js.
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// La fonction proxy s'exécute avant chaque requête
6export function proxy(request: NextRequest) {
7 // Ici, on peut implémenter notre logique
8 console.log('Proxy executed for:', request.nextUrl.pathname);
9
10 // On peut modifier l'objet de la réponse
11 return NextResponse.next();
12}
13
14// Optionnellement, on peut limiter les routes pour lesquelles le proxy s'exécute
15export const config = {
16 matcher: '/api/:path*',
17};Opérations disponibles avec le middleware
1. Redirects et rewrites
Tout comme à Metropolis Quantum, où les portes de sécurité peuvent automatiquement rediriger les visiteurs non autorisés vers des points de contrôle, le middleware dans Next.js permet de rediriger les requêtes :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Vérifie si l'utilisateur essaie d'accéder à la zone admin
7 if (request.nextUrl.pathname.startsWith('/admin')) {
8 // Vérifie si l'utilisateur est connecté (par ex. en vérifiant un cookie)
9 const isLoggedIn = request.cookies.has('auth-token');
10
11 if (!isLoggedIn) {
12 // Redirige les utilisateurs non authentifiés vers la page de connexion
13 const loginUrl = new URL('/login', request.url);
14 // Ajoute l'URL d'origine en paramètre pour pouvoir revenir après la connexion
15 loginUrl.searchParams.set('from', request.nextUrl.pathname);
16 return NextResponse.redirect(loginUrl);
17 }
18 }
19
20 // Sinon, on continue le traitement normal de la requête
21 return NextResponse.next();
22}2. Modifier les en-têtes de la requête ou de la réponse
On peut aussi modifier les en-têtes, tout comme la Quantum Gateway peut ajouter des identifiants spéciaux aux billets de transport :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Récupère la réponse d'origine
7 const response = NextResponse.next();
8
9 // Ajoute des en-têtes personnalisés à la réponse
10 response.headers.set('x-quantum-security', 'enabled');
11 response.headers.set('x-quantum-gateway-version', '15.0.1');
12
13 // Ajoute une Content Security Policy (CSP)
14 response.headers.set(
15 'Content-Security-Policy',
16 "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' quantum-cdn.example.com;"
17 );
18
19 return response;
20}3. Manipuler les cookies
Le middleware peut aussi manipuler les cookies, ce qui est utile pour la gestion des sessions :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const response = NextResponse.next();
7
8 // Lit la valeur d'un cookie
9 const theme = request.cookies.get('theme')?.value;
10
11 // Définit un nouveau cookie
12 response.cookies.set('last-visit', new Date().toISOString());
13
14 // Modifie la valeur d'un cookie existant
15 if (theme === 'dark') {
16 response.cookies.set('theme-version', 'dark-v2');
17 }
18
19 // Supprime un cookie
20 if (request.nextUrl.pathname === '/logout') {
21 response.cookies.delete('auth-token');
22 }
23
24 return response;
25}4. Rewrite - substitution d'URL sans changer l'adresse visible
Rewrite permet de substituer l'URL en interne sans changer l'adresse visible par l'utilisateur :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Exemple : rediriger en interne toutes les requêtes de /docs vers /documentation
7 // L'adresse dans la barre du navigateur reste /docs
8 if (request.nextUrl.pathname.startsWith('/docs')) {
9 const url = request.nextUrl.clone();
10 url.pathname = url.pathname.replace(/^\/docs/, '/documentation');
11 return NextResponse.rewrite(url);
12 }
13
14 return NextResponse.next();
15}Implémenter un middleware pour l'application Quantum Voyages
Préparons un middleware complet pour notre application Quantum Voyages qui gérera :
- L'autorisation pour les zones protégées
- L'internationalisation et la détection de la langue
- Le suivi et l'analytique
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { verifyAuthToken } from './lib/auth';
5
6// Liste des langues prises en charge
7const SUPPORTED_LOCALES = ['pl', 'en', 'de', 'fr'];
8// Langue par défaut
9const DEFAULT_LOCALE = 'pl';
10// Chemins nécessitant une autorisation
11const PROTECTED_PATHS = ['/dashboard', '/account', '/bookings'];
12// Chemins admin nécessitant des permissions d'administrateur
13const ADMIN_PATHS = ['/admin'];
14
15export async function proxy(request: NextRequest) {
16 const { pathname } = request.nextUrl;
17
18 // 1. Gestion de la localisation et de l'internationalisation
19 const pathnameHasLocale = SUPPORTED_LOCALES.some(
20 locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
21 );
22
23 // Si le chemin ne contient pas encore de code de langue, on redirige et on ajoute le code
24 if (!pathnameHasLocale) {
25 // Tentative de détection de la langue préférée de l'utilisateur
26 const preferredLocale = getPreferredLocale(request);
27 const url = new URL(request.url);
28 url.pathname = `/${preferredLocale}${pathname}`;
29 return NextResponse.redirect(url);
30 }
31
32 // Extrait le code de langue du chemin
33 const locale = pathname.split('/')[1];
34
35 // 2. Autorisation pour les zones protégées
36 // Vérifie si le chemin est protégé
37 if (isProtectedPath(pathname)) {
38 const authToken = request.cookies.get('auth-token')?.value;
39
40 // Vérifie le token d'autorisation
41 const authResult = await verifyAuthToken(authToken);
42
43 // Si l'utilisateur n'est pas connecté ou si le token est invalide
44 if (!authResult.isValid) {
45 const loginUrl = new URL(`/${locale}/login`, request.url);
46 loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
47 return NextResponse.redirect(loginUrl);
48 }
49
50 // Vérifie les permissions admin pour les chemins administratifs
51 if (isAdminPath(pathname) && !authResult.isAdmin) {
52 const accessDeniedUrl = new URL(`/${locale}/access-denied`, request.url);
53 return NextResponse.redirect(accessDeniedUrl);
54 }
55 }
56
57 // 3. Ajoute les en-têtes d'analytique et de sécurité
58 const response = NextResponse.next();
59
60 // Ajoute les infos du visiteur pour les outils d'analytique (sans données personnelles)
61 const visitorId = request.cookies.get('visitor-id')?.value || generateVisitorId();
62 response.cookies.set('visitor-id', visitorId, {
63 httpOnly: true,
64 sameSite: 'strict',
65 maxAge: 60 * 60 * 24 * 365 // 1 an
66 });
67
68 // En-têtes de sécurité
69 response.headers.set('X-Frame-Options', 'DENY');
70 response.headers.set('X-Content-Type-Options', 'nosniff');
71 response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
72
73 return response;
74}
75
76// Fonctions d'aide
77
78// Détecte la langue préférée de l'utilisateur
79function getPreferredLocale(request: NextRequest): string {
80 // Vérifie la langue préférée précédemment enregistrée dans un cookie
81 const savedLocale = request.cookies.get('preferred-locale')?.value;
82 if (savedLocale && SUPPORTED_LOCALES.includes(savedLocale)) {
83 return savedLocale;
84 }
85
86 // Vérifie l'en-tête Accept-Language
87 const acceptLanguage = request.headers.get('accept-language');
88 if (acceptLanguage) {
89 // Parse l'en-tête Accept-Language et trouve la meilleure correspondance
90 const userLocales = acceptLanguage.split(',')
91 .map(item => {
92 const [locale, priority = 'q=1.0'] = item.trim().split(';');
93 const q = parseFloat(priority.replace('q=', '')) || 1.0;
94 return { locale: locale.split('-')[0], q };
95 })
96 .sort((a, b) => b.q - a.q);
97
98 // Trouve la première langue prise en charge
99 for (const { locale } of userLocales) {
100 if (SUPPORTED_LOCALES.includes(locale)) {
101 return locale;
102 }
103 }
104 }
105
106 // Langue par défaut si les préférences ne peuvent pas être déterminées
107 return DEFAULT_LOCALE;
108}
109
110// Vérifie si le chemin est protégé
111function isProtectedPath(pathname: string): boolean {
112 return PROTECTED_PATHS.some(path => {
113 // Retire le code de langue du chemin avant la comparaison
114 const pathWithoutLocale = pathname.split('/').slice(2).join('/');
115 return `/${pathWithoutLocale}`.startsWith(path);
116 });
117}
118
119// Vérifie si le chemin est administratif
120function isAdminPath(pathname: string): boolean {
121 return ADMIN_PATHS.some(path => {
122 // Retire le code de langue du chemin avant la comparaison
123 const pathWithoutLocale = pathname.split('/').slice(2).join('/');
124 return `/${pathWithoutLocale}`.startsWith(path);
125 });
126}
127
128// Génère un ID unique pour le visiteur
129function generateVisitorId(): string {
130 return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
131}
132
133// Détermine les chemins pour lesquels le proxy s'exécutera
134export const config = {
135 matcher: [
136 // Correspond à tous les chemins
137 '/((?!api|_next/static|_next/image|favicon.ico).*)',
138 ],
139};Exécution sélective du middleware avec Matcher
Tout comme à Metropolis Quantum, où chaque déplacement ne nécessite pas un contrôle de sécurité complet, dans Next.js 16 on peut spécifier les chemins pour lesquels le proxy s'exécutera à l'aide de la configuration matcher :
1// proxy.ts
2export const config = {
3 matcher: [
4 // Chemins pour lesquels le proxy s'exécutera
5 '/dashboard/:path*',
6 '/api/:path*',
7 '/((?!_next/static|_next/image|favicon.ico).*)',
8 ],
9};Différents modèles de matcher sont disponibles :
- Correspondance exacte :
'/about' - Correspondance avec paramètres :
'/blog/:path' - Correspondance avec paramètres catch-all :
'/blog/:path*' - Plusieurs chemins à la fois : un tableau
['/dashboard/:path*', '/admin/:path*']ou un groupe entre parenthèses'/(dashboard|admin)/:path*' - Correspondance par négation :
/((?!api|_next/static|_next/image|favicon.ico).*)
Implémenter l'autorisation JWT avec le middleware
À Metropolis Quantum, les identifiants des habitants sont vérifiés à l'aide d'une technologie cryptographique avancée. Implémentons un système d'autorisation JWT similaire dans notre application :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { jwtVerify } from 'jose';
5
6interface UserPayload {
7 id: string;
8 email: string;
9 role: string;
10 exp: number;
11}
12
13export async function proxy(request: NextRequest) {
14 // Vérifie si le chemin nécessite une autorisation
15 if (request.nextUrl.pathname.startsWith('/dashboard') ||
16 request.nextUrl.pathname.startsWith('/api/protected')) {
17
18 // Récupère le token JWT depuis le cookie ou l'en-tête Authorization
19 const authToken = request.cookies.get('auth-token')?.value ||
20 request.headers.get('Authorization')?.split(' ')[1];
21
22 if (!authToken) {
23 // Pas de token - redirige vers la connexion
24 return redirectToLogin(request);
25 }
26
27 try {
28 // Vérifie le token JWT
29 const secretKey = new TextEncoder().encode(process.env.JWT_SECRET!);
30 const { payload } = await jwtVerify<UserPayload>(authToken, secretKey);
31
32 // Vérifie si le token a expiré
33 const currentTime = Math.floor(Date.now() / 1000);
34 if (payload.exp < currentTime) {
35 // Token expiré - redirige vers la connexion
36 return redirectToLogin(request);
37 }
38
39 // Vérifie les permissions admin pour les chemins administratifs
40 if (request.nextUrl.pathname.startsWith('/admin') && payload.role !== 'admin') {
41 return NextResponse.json(
42 { message: 'Insufficient permissions' },
43 { status: 403 }
44 );
45 }
46
47 // Ajoute les infos de l'utilisateur aux en-têtes pour les handlers suivants
48 const response = NextResponse.next();
49 response.headers.set('x-user-id', payload.id);
50 response.headers.set('x-user-role', payload.role);
51
52 return response;
53 } catch (error) {
54 // Erreur de vérification du token - redirige vers la connexion
55 return redirectToLogin(request);
56 }
57 }
58
59 return NextResponse.next();
60}
61
62// Fonction qui redirige vers la connexion
63function redirectToLogin(request: NextRequest) {
64 const loginUrl = new URL('/login', request.url);
65 loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
66 return NextResponse.redirect(loginUrl);
67}
68
69export const config = {
70 matcher: [
71 '/dashboard/:path*',
72 '/admin/:path*',
73 '/api/protected/:path*',
74 ],
75};Implémenter le changement de thème avec le middleware
La Quantum City offre à ses habitants la possibilité de personnaliser l'apparence des interfaces de la ville. Implémentons un système similaire pour basculer entre les thèmes clair et sombre :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Récupère le thème préféré
7 const theme = request.cookies.get('theme')?.value || 'light';
8
9 // Crée la réponse
10 const response = NextResponse.next();
11
12 // Définit la variable de préférence de thème sous forme de cookie
13 response.cookies.set('theme', theme, {
14 maxAge: 60 * 60 * 24 * 365, // valable un an
15 path: '/',
16 });
17
18 // Ajoute un en-tête personnalisé lisible par le client
19 response.headers.set('x-theme', theme);
20
21 return response;
22}
23
24export const config = {
25 matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
26};Gérer plusieurs domaines et sous-domaines avec le middleware
À Metropolis Quantum, différents districts peuvent avoir leurs propres systèmes d'accès. De la même manière dans Next.js, on peut gérer différents domaines et sous-domaines :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const { pathname, hostname } = request.nextUrl;
7
8 // Gère les différents sous-domaines
9 if (hostname === 'admin.quantumvoyages.com') {
10 // Redirige toutes les requêtes vers le panneau d'administration
11 const url = new URL('/admin' + pathname, request.url);
12 return NextResponse.rewrite(url);
13 }
14
15 if (hostname === 'api.quantumvoyages.com') {
16 // Redirige les requêtes vers l'API
17 const url = new URL('/api' + pathname, request.url);
18 return NextResponse.rewrite(url);
19 }
20
21 if (hostname.startsWith('user-')) {
22 // Gère les sous-domaines utilisateur (par ex. user-john.quantumvoyages.com)
23 const username = hostname.replace('user-', '').split('.')[0];
24 const url = new URL(`/profile/${username}${pathname}`, request.url);
25 return NextResponse.rewrite(url);
26 }
27
28 return NextResponse.next();
29}
30
31export const config = {
32 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
33};Mise en cache et modification des en-têtes de réponse
À Metropolis Quantum, le système Gateway optimise le flux de données à travers ses systèmes. De la même manière, on peut optimiser notre application :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const response = NextResponse.next();
7
8 // Définit les en-têtes de cache pour les ressources statiques
9 if (request.nextUrl.pathname.match(/\.(jpg|jpeg|png|webp|svg|gif|ico)$/)) {
10 response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
11 }
12
13 // Définit les en-têtes de cache pour les pages HTML
14 else if (!request.nextUrl.pathname.startsWith('/api/')) {
15 response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
16 }
17
18 // Ajoute les en-têtes de sécurité
19 response.headers.set('Strict-Transport-Security', 'max-age=63072000');
20 response.headers.set('X-Content-Type-Options', 'nosniff');
21 response.headers.set('X-Frame-Options', 'DENY');
22 response.headers.set('X-XSS-Protection', '1; mode=block');
23
24 return response;
25}
26
27export const config = {
28 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
29};Implémenter la géolocalisation et la personnalisation du contenu
Metropolis Quantum adapte les services disponibles en fonction de la localisation des habitants. Implémentons un système similaire dans notre application, en utilisant le middleware pour déterminer la localisation de l'utilisateur :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Table des régions et de leurs destinations disponibles
6const REGION_DESTINATIONS = {
7 'EU': ['mars', 'europa', 'titan'],
8 'NA': ['mars', 'luna', 'titan', 'ganymede'],
9 'ASIA': ['mars', 'venus-orbiter', 'luna'],
10 'default': ['mars'] // destinations par défaut disponibles partout
11};
12
13export function proxy(request: NextRequest) {
14 // Récupère la région de l'utilisateur (en réalité, tu pourrais utiliser une API de géolocalisation externe)
15 const countryCode = request.headers.get('x-country') || 'default';
16 const region = getRegionFromCountry(countryCode);
17
18 // Crée la réponse
19 const response = NextResponse.next();
20
21 // Ajoute les infos de région et les destinations disponibles aux en-têtes
22 response.headers.set('x-user-region', region);
23 response.headers.set('x-available-destinations', JSON.stringify(REGION_DESTINATIONS[region] || REGION_DESTINATIONS.default));
24
25 // Définit le cookie de région (sera disponible dans le navigateur)
26 response.cookies.set('user-region', region, {
27 maxAge: 60 * 60 * 24 * 30, // 30 jours
28 path: '/',
29 });
30
31 return response;
32}
33
34// Fonction associant les codes pays aux régions
35function getRegionFromCountry(countryCode: string): keyof typeof REGION_DESTINATIONS {
36 const EU_COUNTRIES = ['DE', 'FR', 'IT', 'ES', 'PL', 'NL', 'BE', 'SE', 'AT', 'DK'];
37 const NA_COUNTRIES = ['US', 'CA', 'MX'];
38 const ASIA_COUNTRIES = ['JP', 'CN', 'KR', 'IN', 'SG', 'TH', 'MY', 'PH', 'VN'];
39
40 if (EU_COUNTRIES.includes(countryCode)) return 'EU';
41 if (NA_COUNTRIES.includes(countryCode)) return 'NA';
42 if (ASIA_COUNTRIES.includes(countryCode)) return 'ASIA';
43
44 return 'default';
45}
46
47export const config = {
48 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
49};Limitation du débit des requêtes
À Metropolis Quantum, le système Gateway contrôle le nombre de requêtes vers les ressources critiques pour éviter les surcharges. Implémentons un mécanisme de rate limiting similaire :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Système simple de rate limiting basé sur la mémoire (en production, mieux vaut utiliser Redis ou un service similaire)
6const RATE_LIMIT_DURATION = 60 * 1000; // 1 minute
7const MAX_REQUESTS_PER_MINUTE = 60; // Nombre maximum de requêtes par minute
8
9// Stockage des compteurs de requêtes pour différentes IP (dans une vraie application, mieux vaut utiliser Redis)
10const ipRequestCounts = new Map<string, { count: number, resetAt: number }>();
11
12export function proxy(request: NextRequest) {
13 // On ne limite que les requêtes vers l'API
14 if (request.nextUrl.pathname.startsWith('/api/')) {
15 // NextRequest n'a plus de champ ip (supprimé dans Next.js 15) : l'en-tête x-forwarded-for fournit l'adresse du client
16 const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown';
17 const now = Date.now();
18
19 // Récupère ou crée un compteur pour cette IP
20 if (!ipRequestCounts.has(ip) || ipRequestCounts.get(ip)!.resetAt < now) {
21 // Réinitialise le compteur si le temps est écoulé
22 ipRequestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_DURATION });
23 } else {
24 // Incrémente le compteur
25 const record = ipRequestCounts.get(ip)!;
26 record.count++;
27
28 // Vérifie si la limite a été dépassée
29 if (record.count > MAX_REQUESTS_PER_MINUTE) {
30 // Calcule le temps après lequel les requêtes peuvent à nouveau être envoyées
31 const resetTime = Math.ceil((record.resetAt - now) / 1000);
32
33 // Renvoie une réponse avec le code 429 Too Many Requests
34 return new NextResponse('Rate limit exceeded', {
35 status: 429,
36 headers: {
37 'Retry-After': `${resetTime}`,
38 'X-RateLimit-Limit': `${MAX_REQUESTS_PER_MINUTE}`,
39 'X-RateLimit-Remaining': '0',
40 'X-RateLimit-Reset': `${resetTime}`,
41 },
42 });
43 }
44 }
45 }
46
47 return NextResponse.next();
48}
49
50export const config = {
51 matcher: '/api/:path*',
52};Tests A/B avec le middleware
À Metropolis Quantum, certaines nouvelles fonctionnalités sont testées sur des groupes d'habitants sélectionnés. Implémentons un système de tests A/B similaire :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Définitions des tests A/B
6const AB_TESTS = {
7 'new-landing-page': {
8 variants: ['control', 'variant-a', 'variant-b'],
9 weights: [0.5, 0.25, 0.25], // 50% des utilisateurs verront control, 25% variant-a, 25% variant-b
10 },
11 'checkout-flow': {
12 variants: ['standard', 'simplified'],
13 weights: [0.5, 0.5], // 50% des utilisateurs verront chaque variante
14 },
15};
16
17export function proxy(request: NextRequest) {
18 const response = NextResponse.next();
19
20 // Assigne l'utilisateur aux groupes de test
21 Object.entries(AB_TESTS).forEach(([testName, test]) => {
22 // Récupère la variante existante depuis le cookie ou en assigne une nouvelle
23 let variant = request.cookies.get(`ab-${testName}`)?.value;
24
25 if (!variant || !test.variants.includes(variant)) {
26 // Assigne une variante en fonction des poids
27 variant = assignVariant(test.variants, test.weights);
28
29 // Enregistre la variante dans un cookie
30 response.cookies.set(`ab-${testName}`, variant, {
31 maxAge: 60 * 60 * 24 * 30, // 30 jours
32 path: '/',
33 });
34 }
35
36 // Ajoute l'info de variante sous forme d'en-tête pour le client
37 response.headers.set(`x-ab-${testName}`, variant);
38 });
39
40 return response;
41}
42
43// Fonction assignant une variante en fonction des poids
44function assignVariant(variants: string[], weights: number[]): string {
45 const random = Math.random();
46 let cumulativeWeight = 0;
47
48 for (let i = 0; i < variants.length; i++) {
49 cumulativeWeight += weights[i];
50 if (random < cumulativeWeight) {
51 return variants[i];
52 }
53 }
54
55 // Par défaut, la première variante
56 return variants[0];
57}
58
59export const config = {
60 matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
61};Surveiller et déboguer le middleware
Dans les applications en production, il est important de surveiller le comportement du middleware. On peut implémenter un système de journalisation simple :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const startTime = Date.now();
7
8 // Crée un identifiant de requête
9 const requestId = generateRequestId();
10
11 // Crée la réponse
12 const response = NextResponse.next();
13
14 // Ajoute l'identifiant de requête aux en-têtes
15 response.headers.set('x-request-id', requestId);
16
17 // Temps passé dans le proxy lui-même (la page n'est rendue qu'après lui, ce n'est donc pas la durée de toute la requête)
18 const duration = Date.now() - startTime;
19 response.headers.set('x-response-time', `${duration}ms`);
20
21 // En production, tu pourrais envoyer ces données à un système de monitoring
22 // logRequest(requestId, request.method, request.url, response.status, duration);
23
24 return response;
25}
26
27// Génère un identifiant de requête unique
28function generateRequestId(): string {
29 return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
30}
31
32export const config = {
33 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
34};Intégration avec des systèmes externes
Le middleware peut aussi s'intégrer avec des systèmes externes, comme des systèmes d'authentification ou d'analytique :
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export async function proxy(request: NextRequest) {
6 // Récupère les infos de session depuis un service externe
7 const sessionId = request.cookies.get('session-id')?.value;
8 let userInfo = null;
9
10 if (sessionId) {
11 try {
12 // Dans une vraie application, cela pourrait être un appel d'API
13 userInfo = await fetchUserInfo(sessionId);
14 } catch (error) {
15 console.error('Error fetching user info:', error);
16 }
17 }
18
19 // Crée la réponse
20 const response = NextResponse.next();
21
22 // Si on a les infos de l'utilisateur, on les ajoute aux en-têtes
23 if (userInfo) {
24 response.headers.set('x-user-id', userInfo.id);
25 response.headers.set('x-user-plan', userInfo.plan);
26
27 // Envoie les données au système d'analytique
28 trackUserActivity(userInfo.id, request.url, request.method);
29 }
30
31 return response;
32}
33
34// Fonction récupérant les infos de l'utilisateur (simulation)
35async function fetchUserInfo(sessionId: string) {
36 // Dans une vraie application, cela pourrait être un appel d'API
37 return new Promise(resolve => {
38 setTimeout(() => {
39 resolve({
40 id: 'user-123',
41 plan: 'premium',
42 name: 'John Doe'
43 });
44 }, 50);
45 });
46}
47
48// Fonction suivant l'activité de l'utilisateur (simulation)
49function trackUserActivity(userId: string, url: string, method: string) {
50 // Dans une vraie application, cela pourrait envoyer des données à Google Analytics, Mixpanel, etc.
51 console.log(`User ${userId} accessed ${url} with method ${method}`);
52}
53
54export const config = {
55 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
56};Résumé
Le proxy dans Next.js 16 (anciennement middleware) est un outil puissant qui, comme la Quantum Gateway à Metropolis Quantum, permet de contrôler et de modifier le trafic de l'application dès le tout début du cycle requête-réponse. Grâce à lui, on peut :
Implémenter des systèmes d'autorisation avancés - vérifier les tokens JWT, rediriger les utilisateurs non authentifiés, contrôler l'accès aux ressources protégées.
Ajouter des fonctionnalités d'internationalisation - détecter la langue préférée de l'utilisateur, rediriger vers les versions linguistiques appropriées.
Personnaliser l'expérience utilisateur - adapter le contenu en fonction de la localisation, des préférences et de l'historique de l'utilisateur.
Optimiser les performances - contrôler les en-têtes de cache, modifier les réponses, implémenter le rate limiting.
S'intégrer avec des systèmes externes - se connecter à des services d'analytique, d'authentification et autres.
Réaliser des tests A/B - assigner les utilisateurs à des groupes de test et personnaliser l'expérience.
Surveiller et déboguer - suivre les requêtes, mesurer les performances, enregistrer les métriques.
En tirant parti du middleware, on peut considérablement étendre les capacités de notre application Next.js 16, en ajoutant des fonctionnalités avancées qui s'exécutent tôt dans le cycle de vie de la requête, ce qui permet un meilleur contrôle et de meilleures performances.
Dans la prochaine leçon, tu découvriras le préchargement et d'autres façons d'accélérer la navigation dans Next.js 16.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Les métadonnées dans l'App Router se définissent par :
2. Les Private Folders dans l'App Router se marquent avec le préfixe :
3. Par défaut, les composants de l'App Router sans la directive 'use client' :
4. Pour créer un Client Component, il faut ajouter :
5. Qu'est-ce que le prefetching dans Next.js ?
6. Dans Next.js 16, comment doit s'appeler le fichier à la racine du projet qui a remplacé middleware.ts et s'exécute avant le traitement d'une requête ?
7. En quoi router.replace() diffère-t-il de router.push() du hook useRouter ?
8. Quel préfixe une variable d'environnement doit-elle avoir pour être accessible côté client dans Next.js ?
9. Les Parallel Routes dans l'App Router utilisent la convention de nommage :
10. Que se passe-t-il lorsqu'on appelle la fonction notFound() dans un Server Component sous Next.js 16 ?
11. Pourquoi le fichier error.tsx dans Next.js 16 doit-il être un Client Component (avec la directive 'use client') ?
12. À quoi le fichier template.tsx convient-il le mieux, à la place de layout.tsx ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée la route catch-all app/docs/[...slug]/page.js : exporte par défaut un composant async DocsPage({ params }), lis le tableau des segments avec const { slug } = await params et affiche chaque segment de l'URL dans son propre élément li d'une liste ul.
- Tri vertical
Remets dans l'ordre la gestion d'une erreur par le fichier error.js
- Clique dans l'ordre
Reconstitue le chemin de fichier correct pour une route catch-all dans Next.js
- Tri vertical
Reconstitue la syntaxe du root layout (app/layout.tsx) dans l'App Router
- Tri vertical
Remets dans l'ordre la génération de pages statiques dans l'App Router
- Tri vertical
Reconstitue la syntaxe du root layout avec les balises html et body
- Clique dans l'ordre
Reconstitue le chemin correct vers une page produit dynamique dans l'App Router
- Tri vertical
Remets dans l'ordre le processus d'Incremental Static Regeneration
- Clique dans l'ordre
Reconstitue la syntaxe de usePathname dans un composant
- Tri vertical
Remets dans l'ordre les étapes de la navigation avec le composant Link : de l'apparition du lien à l'écran à la mise à jour de la page
- Tri vertical
Remets dans l'ordre la résolution des métadonnées dans Next.js
- Tri vertical
Reconstitue la syntaxe d'import et d'utilisation du hook useRouter
- Clique dans l'ordre
Reconstitue la syntaxe de generateMetadata
- Tri vertical
Remets dans l'ordre les étapes de l'optimisation d'une police avec next/font (du build à l'affichage du texte)
- Tri vertical
Reconstitue la syntaxe d'utilisation du composant Script de Next.js
- Tri vertical
Remets dans l'ordre le rendu des Server Components