DevToolBoxGRATUIT
Blog

Next.js Middleware : Authentification, Redirections et Rate Limiting

14 minpar DevToolBox

Le middleware Next.js s'exécute avant qu'une requête ne soit terminée, sur l'Edge Runtime, idéal pour l'authentification, les redirections géographiques, les tests A/B et la limitation de débit.

Qu'est-ce que le Middleware Next.js ?

Le middleware est une fonction qui s'exécute avant que la requête n'atteigne votre page.

// Request flow in Next.js
//
// Client Request
//     │
//     ▼
// ┌─────────────┐
// │  Middleware  │  ← Runs here (Edge Runtime)
// └─────────────┘
//     │
//     ▼
// ┌─────────────┐
// │  Page/API    │  ← Your application code
// └─────────────┘
//     │
//     ▼
// Client Response

Configuration de base

Créez un fichier middleware.ts à la racine de votre projet.

// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Log every request
  console.log(`[${request.method}] ${request.nextUrl.pathname}`);

  // Continue to the next handler
  return NextResponse.next();
}

// Only run on specific paths
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Configuration du Matcher

Le matcher détermine quelles routes déclenchent le middleware.

// Match specific routes
export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
};

// Match all routes except static files and API
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

// Multiple matchers with different patterns
export const config = {
  matcher: [
    '/dashboard/:path*',     // All dashboard routes
    '/admin/:path*',         // All admin routes
    '/api/protected/:path*', // Protected API routes
    '/profile',              // Exact match
  ],
};

// Conditional matching with has/missing (Next.js 13.1+)
export const config = {
  matcher: [
    {
      source: '/api/:path*',
      has: [
        { type: 'header', key: 'authorization' },
      ],
    },
  ],
};

Middleware d'authentification

Protection des routes nécessitant une authentification.

Vérification JWT

Vérifiez les tokens JWT directement dans le middleware.

// middleware.ts — JWT Authentication
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET!
);

const PUBLIC_PATHS = ['/login', '/register', '/forgot-password'];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Skip public paths
  if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  // Get token from cookie or Authorization header
  const token =
    request.cookies.get('auth-token')?.value ||
    request.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    // Redirect to login with return URL
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    // Verify JWT token
    const { payload } = await jwtVerify(token, JWT_SECRET);

    // Add user info to headers for downstream use
    const response = NextResponse.next();
    response.headers.set('x-user-id', payload.sub as string);
    response.headers.set('x-user-role', payload.role as string);
    return response;
  } catch (error) {
    // Token expired or invalid — redirect to login
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete('auth-token');
    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*', '/api/protected/:path*'],
};

Contrôle d'accès par rôle

Contrôle d'accès basé sur les rôles utilisateur.

// Role-based access control in middleware
const ROUTE_PERMISSIONS: Record<string, string[]> = {
  '/admin':        ['admin'],
  '/dashboard':    ['admin', 'editor', 'viewer'],
  '/editor':       ['admin', 'editor'],
  '/api/admin':    ['admin'],
};

function hasPermission(pathname: string, role: string): boolean {
  for (const [route, roles] of Object.entries(ROUTE_PERMISSIONS)) {
    if (pathname.startsWith(route)) {
      return roles.includes(role);
    }
  }
  return true; // No restriction for unmatched routes
}

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  if (!token) return NextResponse.redirect(new URL('/login', request.url));

  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);
    const role = payload.role as string;
    const { pathname } = request.nextUrl;

    if (!hasPermission(pathname, role)) {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }

    return NextResponse.next();
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

Modèles de redirection

Idéal pour les redirections conditionnelles.

Redirections géographiques

Redirigez les utilisateurs selon leur géolocalisation.

// Geo-based redirect (Vercel Edge)
export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';
  const city = request.geo?.city || 'Unknown';

  // Redirect EU users to GDPR-compliant page
  const EU_COUNTRIES = [
    'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI',
    'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU',
    'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
  ];

  if (EU_COUNTRIES.includes(country) &&
      request.nextUrl.pathname === '/') {
    return NextResponse.redirect(new URL('/eu', request.url));
  }

  // Add geo headers for downstream use
  const response = NextResponse.next();
  response.headers.set('x-country', country);
  response.headers.set('x-city', city);
  return response;
}

Détection de locale

Détectez la langue préférée et redirigez.

// Locale detection middleware
const SUPPORTED_LOCALES = ['en', 'fr', 'de', 'es', 'ja', 'ko', 'zh'];
const DEFAULT_LOCALE = 'en';

function getPreferredLocale(request: NextRequest): string {
  // Check cookie first (user preference)
  const cookieLocale = request.cookies.get('locale')?.value;
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
    return cookieLocale;
  }

  // Parse Accept-Language header
  const acceptLang = request.headers.get('accept-language') || '';
  const preferred = acceptLang
    .split(',')
    .map(lang => lang.split(';')[0].trim().substring(0, 2))
    .find(lang => SUPPORTED_LOCALES.includes(lang));

  return preferred || DEFAULT_LOCALE;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Check if locale is already in the path
  const hasLocale = SUPPORTED_LOCALES.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (hasLocale) return NextResponse.next();

  // Redirect to locale-prefixed path
  const locale = getPreferredLocale(request);
  const url = new URL(`/${locale}${pathname}`, request.url);
  return NextResponse.redirect(url);
}

Limitation de débit

Implémentez la limitation de débit en bordure.

// Simple rate limiting with in-memory store
// Note: This works per-edge-node; use Redis for distributed rate limiting

const rateLimit = new Map<string, { count: number; resetTime: number }>();

const RATE_LIMIT = 100;       // requests
const RATE_WINDOW = 60 * 1000; // per minute

function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const record = rateLimit.get(ip);

  if (!record || now > record.resetTime) {
    rateLimit.set(ip, { count: 1, resetTime: now + RATE_WINDOW });
    return false;
  }

  record.count++;
  return record.count > RATE_LIMIT;
}

export function middleware(request: NextRequest) {
  // Only rate-limit API routes
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ||
             request.ip ||
             '127.0.0.1';

  if (isRateLimited(ip)) {
    return new NextResponse(
      JSON.stringify({ error: 'Too many requests' }),
      {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
          'Retry-After': '60',
        },
      }
    );
  }

  return NextResponse.next();
}

Fenêtre glissante

Algorithme de fenêtre glissante pour lisser la limitation.

// Rate limiting with Upstash Redis (production-ready)
// npm install @upstash/ratelimit @upstash/redis

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
  analytics: true,
  prefix: 'api-ratelimit',
});

export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const ip = request.ip ?? '127.0.0.1';
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  const response = success
    ? NextResponse.next()
    : new NextResponse(JSON.stringify({ error: 'Rate limit exceeded' }), {
        status: 429,
        headers: { 'Content-Type': 'application/json' },
      });

  // Always set rate limit headers
  response.headers.set('X-RateLimit-Limit', limit.toString());
  response.headers.set('X-RateLimit-Remaining', remaining.toString());
  response.headers.set('X-RateLimit-Reset', reset.toString());

  return response;
}

En-têtes et sécurité

Endroit idéal pour les en-têtes de sécurité.

En-têtes de sécurité

// Security headers middleware
export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Prevent XSS attacks
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
  );

  // Prevent clickjacking
  response.headers.set('X-Frame-Options', 'DENY');

  // Prevent MIME type sniffing
  response.headers.set('X-Content-Type-Options', 'nosniff');

  // Enable HSTS
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=63072000; includeSubDomains; preload'
  );

  // Referrer policy
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions policy
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );

  return response;
}

En-têtes CORS

Gérez CORS au niveau du middleware.

// CORS middleware for API routes
const ALLOWED_ORIGINS = [
  'https://myapp.com',
  'https://staging.myapp.com',
  process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '',
].filter(Boolean);

export function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const origin = request.headers.get('origin') || '';
  const isAllowed = ALLOWED_ORIGINS.includes(origin);

  // Handle preflight OPTIONS request
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': isAllowed ? origin : '',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  const response = NextResponse.next();
  if (isAllowed) {
    response.headers.set('Access-Control-Allow-Origin', origin);
    response.headers.set('Access-Control-Allow-Credentials', 'true');
  }
  return response;
}

Tests A/B

Tests A/B côté serveur sans scintillement.

// A/B Testing middleware
export function middleware(request: NextRequest) {
  // Only A/B test the homepage
  if (request.nextUrl.pathname !== '/') {
    return NextResponse.next();
  }

  // Check if user already has a variant assigned
  const variant = request.cookies.get('ab-variant')?.value;

  if (variant) {
    // Rewrite to the variant page without changing URL
    return NextResponse.rewrite(
      new URL(`/variants/${variant}`, request.url)
    );
  }

  // Assign a random variant (50/50 split)
  const newVariant = Math.random() < 0.5 ? 'control' : 'experiment';

  const response = NextResponse.rewrite(
    new URL(`/variants/${newVariant}`, request.url)
  );

  // Persist variant in cookie for 30 days
  response.cookies.set('ab-variant', newVariant, {
    maxAge: 60 * 60 * 24 * 30,
    httpOnly: true,
    sameSite: 'lax',
  });

  return response;
}

Réécriture d'URL

Réécrivez sans changer l'URL du navigateur.

// Multi-tenant rewriting
// app.mysite.com → /tenants/app
// docs.mysite.com → /tenants/docs
export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';
  const subdomain = hostname.split('.')[0];

  if (subdomain && subdomain !== 'www' && subdomain !== 'mysite') {
    return NextResponse.rewrite(
      new URL(`/tenants/${subdomain}${request.nextUrl.pathname}`, request.url)
    );
  }

  return NextResponse.next();
}

// Proxy API requests to external service
// /api/external/* → https://api.external-service.com/*
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/external/')) {
    const path = request.nextUrl.pathname.replace('/api/external', '');
    return NextResponse.rewrite(
      new URL(`https://api.external-service.com${path}`)
    );
  }
  return NextResponse.next();
}

Chaînage de middleware

Composez plusieurs fonctions middleware.

// Composing multiple middleware functions
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

type MiddlewareFn = (
  request: NextRequest,
  response: NextResponse
) => NextResponse | Promise<NextResponse>;

function chain(
  functions: MiddlewareFn[],
  request: NextRequest
): Promise<NextResponse> {
  return functions.reduce(
    async (promise, fn) => {
      const response = await promise;
      return fn(request, response);
    },
    Promise.resolve(NextResponse.next())
  );
}

// Individual middleware functions
const withLogging: MiddlewareFn = (request, response) => {
  console.log(`[${request.method}] ${request.nextUrl.pathname}`);
  return response;
};

const withSecurityHeaders: MiddlewareFn = (request, response) => {
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  return response;
};

const withAuth: MiddlewareFn = (request, response) => {
  const token = request.cookies.get('auth-token')?.value;
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return response;
};

// Compose all middleware
export function middleware(request: NextRequest) {
  return chain([withLogging, withSecurityHeaders, withAuth], request);
}

Limitations Edge Runtime

L'Edge Runtime a certaines limitations.

  • Pas d'accès fichier
  • Pas de modules Node.js natifs
  • Web APIs uniquement
  • Taille bundle limitée à 1MB
  • Pas de connexions DB
  • Temps d'exécution limité
// What you CAN use in Edge Runtime:
// ✅ fetch, Request, Response, Headers, URL
// ✅ TextEncoder, TextDecoder
// ✅ crypto.subtle (Web Crypto API)
// ✅ structuredClone
// ✅ atob, btoa
// ✅ URLPattern
// ✅ EdgeDB, Upstash, PlanetScale drivers

// What you CANNOT use:
// ❌ fs (file system)
// ❌ path, os, child_process
// ❌ pg, mysql2, mongoose (traditional DB drivers)
// ❌ bcrypt (use bcryptjs or Web Crypto instead)
// ❌ Large npm packages that exceed 1MB bundle

Débogage

Techniques de débogage pour le middleware.

// Debugging middleware
export function middleware(request: NextRequest) {
  // Log request details
  console.log({
    method: request.method,
    url: request.url,
    pathname: request.nextUrl.pathname,
    cookies: Object.fromEntries(request.cookies.getAll().map(c => [c.name, c.value])),
    headers: Object.fromEntries(request.headers),
    geo: request.geo,
    ip: request.ip,
  });

  // Return debug info in response headers (dev only)
  const response = NextResponse.next();
  if (process.env.NODE_ENV === 'development') {
    response.headers.set('x-middleware-pathname', request.nextUrl.pathname);
    response.headers.set('x-middleware-matched', 'true');
  }
  return response;
}

Bonnes pratiques

  1. Gardez le middleware léger.
  2. Excluez les fichiers statiques.
  3. Évitez les appels DB.
  4. Définissez les en-têtes de cache.
  5. Cookies pour A/B.
  6. Gestion d'erreurs.
  7. Testez différents scénarios.
  8. Variables d'environnement.

Conclusion

Le middleware Next.js est un outil puissant en bordure. Maîtrisez ces modèles pour une application robuste.

FAQ

Le middleware s'exécute-t-il à chaque requête ?

Par défaut oui, utilisez le matcher pour limiter.

Peut-on utiliser une base de données ?

Pas directement. Utilisez fetch vers une route API.

Différence entre redirect et rewrite ?

Redirect change l'URL du navigateur, rewrite non.

Comment gérer les erreurs ?

Utilisez try-catch et retournez une réponse appropriée.

𝕏 Twitterin LinkedIn
Cet article vous a-t-il aidé ?

Restez informé

Recevez des astuces dev et les nouveaux outils chaque semaine.

Pas de spam. Désabonnez-vous à tout moment.

Essayez ces outils associés

{ }JSON FormatterJWTJWT Decoder

Articles connexes

Next.js App Router : Guide de migration complet 2026

Maitrisez le Next.js App Router avec ce guide complet. Server Components, fetching de donnees, layouts, streaming, Server Actions et migration depuis Pages Router.

Authentification JWT : Guide d'implementation complet

Implementez l'authentification JWT de zero. Structure des tokens, access et refresh tokens, implementation Node.js, gestion cote client, bonnes pratiques de securite et middleware Next.js.

React Server Components : Guide Complet 2026

Maîtrisez les React Server Components : architecture, récupération de données et streaming.