Next.js 16 : Migrer de middleware.ts à proxy.ts — Le guide complet

Next.js 16 remplace middleware.ts par proxy.ts avec un runtime Node.js. Découvrez comment migrer, gérer l'authentification, les redirections et les headers de sécurité — avec des exemples de code prêts à l'emploi.

Si vous suivez un minimum l'actualité de Next.js, vous avez forcément entendu parler du changement le plus visible de Next.js 16 : le fichier middleware.ts est officiellement déprécié et remplacé par proxy.ts. Et non, ce n'est pas juste un renommage cosmétique — c'est un vrai changement d'architecture qui touche le runtime, les bonnes pratiques de sécurité, et la façon dont on structure toute la logique d'interception des requêtes.

Concrètement, proxy.ts tourne désormais sur le runtime Node.js (et non plus sur l'Edge Runtime), ce qui ouvre pas mal de nouvelles possibilités mais change aussi certaines contraintes. Dans ce guide, on va couvrir en détail la migration, les cas d'usage concrets avec des exemples de code qui fonctionnent vraiment, et les bonnes pratiques pour l'authentification, les redirections et la gestion des headers. Tous les exemples sont testés avec Next.js 16.1 et React 19.

Pourquoi Next.js a renommé middleware en proxy

Honnêtement, le terme « middleware » posait un vrai problème de compréhension depuis le début. Beaucoup de développeurs — surtout ceux venant d'Express.js — s'attendaient à un système de middleware chaînable, avec next(), des couches empilables, et une logique métier riche. Sauf que le middleware de Next.js n'a jamais fonctionné comme ça. C'était depuis le départ un proxy réseau qui intercepte les requêtes avant qu'elles n'atteignent vos routes.

L'équipe de Vercel a identifié trois raisons principales pour ce changement :

  • Clarté sémantique — Le mot « proxy » décrit précisément ce que fait cette couche : intercepter et transférer des requêtes à la frontière réseau de votre application. Fini l'ambiguïté avec le middleware d'Express.
  • Décourager la surcharge — Trop de projets mettaient de la logique métier lourde dans le middleware (requêtes base de données, validation JWT complète, etc.). Le renommage en « proxy » rappelle que cette couche doit rester légère.
  • Sécurité renforcée — Suite à la vulnérabilité CVE-2025-29927 découverte en mars 2025, qui permettait de contourner toutes les vérifications d'autorisation du middleware via un simple header x-middleware-subrequest, l'équipe Next.js a repensé l'architecture de cette couche. Un changement de nom peut paraître anodin, mais il s'inscrit dans une refonte bien plus profonde.

Les différences concrètes entre middleware.ts et proxy.ts

Avant de se lancer dans la migration, il faut bien comprendre ce qui change réellement. Voici un comparatif :

Caractéristiquemiddleware.ts (déprécié)proxy.ts (Next.js 16)
RuntimeEdge Runtime (par défaut)Node.js (fixe, non configurable)
StatutDépréciéActif / Recommandé
Nom de la fonction exportéemiddleware()proxy()
APIs Node.jsNon disponiblesDisponibles
Config matcherIdentiqueIdentique
NextRequest / NextResponseIdentiqueIdentique

Le point le plus important à retenir : le runtime Node.js est fixe et non configurable dans proxy.ts. Si vous avez absolument besoin de l'Edge Runtime, vous pouvez temporairement garder middleware.ts, mais gardez en tête qu'il sera supprimé dans une future version.

Ce que le runtime Node.js change en pratique

Passer de l'Edge Runtime au runtime Node.js, ça apporte plusieurs avantages concrets :

  • Accès aux APIs Node.js — Vous pouvez utiliser fs, crypto, path et toutes les APIs Node.js natives. C'était tout simplement impossible avec l'Edge Runtime.
  • Compatibilité élargie des packages npm — Plus besoin de vérifier si chaque dépendance est compatible avec l'Edge. La bibliothèque jsonwebtoken, par exemple, fonctionne maintenant directement (même si jose reste recommandé pour les performances).
  • Débogage simplifié — Le runtime Node.js offre un bien meilleur support pour les outils de débogage classiques. Et franchement, quiconque a essayé de déboguer du code sur l'Edge Runtime sait à quel point c'était pénible.

En revanche, vous perdez la distribution géographique automatique de l'Edge. Sur Vercel, vos fonctions proxy tourneront dans la région de votre déploiement plutôt que sur chaque point de présence mondial. Pour la grande majorité des applications, cette différence de latence est négligeable.

Migration pas à pas : du middleware au proxy

Méthode automatique avec le codemod

Bonne nouvelle : Next.js fournit un codemod officiel qui fait tout le travail en une seule commande :

# Migration complète vers Next.js 16
npx @next/codemod upgrade 16

# Ou pour cibler uniquement le renommage middleware → proxy
npx @next/codemod middleware-to-proxy

Le codemod effectue trois opérations automatiquement : il renomme le fichier middleware.ts en proxy.ts, il renomme la fonction exportée de middleware à proxy, et il met à jour les imports si nécessaire. Simple et efficace.

Méthode manuelle

Si vous préférez migrer à la main (ou si le codemod ne couvre pas tous vos cas — ça arrive), voici la procédure complète.

Avant (middleware.ts) :

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Après (proxy.ts) :

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const token = request.cookies.get('session')?.value

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Vous voyez, les modifications sont vraiment minimes : renommer le fichier, renommer la fonction. La config matcher reste strictement identique. Par contre, si vous oubliez de renommer la fonction, vous allez tomber sur cette erreur :

⨯ The file "./src/proxy.ts" must export a function, either as a default export or as a named "proxy" export.

Authentification avec proxy.ts : le bon pattern

L'authentification est de loin le cas d'usage le plus courant du proxy. Mais attention — la philosophie a un peu changé avec Next.js 16. Le proxy doit rester léger : il fait un contrôle d'accès rapide, point final. La validation approfondie se fait ailleurs.

Contrôle d'accès basique avec cookies

Voici le pattern recommandé pour protéger des routes. C'est celui qu'on utilise sur la plupart de nos projets et il couvre 90% des besoins :

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const protectedRoutes = ['/dashboard', '/settings', '/profile']
const authRoutes = ['/login', '/register']

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  const sessionToken = request.cookies.get('session')?.value

  // Rediriger les utilisateurs non authentifiés
  const isProtected = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  )
  if (isProtected && !sessionToken) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }

  // Rediriger les utilisateurs déjà connectés loin des pages auth
  const isAuthRoute = authRoutes.some((route) =>
    pathname.startsWith(route)
  )
  if (isAuthRoute && sessionToken) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

Remarquez qu'on ne valide pas le JWT ici — on vérifie simplement la présence du cookie. C'est un contrôle optimiste. La validation complète du token se fait dans vos Server Components ou Server Actions via votre Data Access Layer.

Pourquoi ne pas valider le JWT dans le proxy ?

C'est une question qui revient tout le temps. Même si le runtime Node.js le permet techniquement, il y a plusieurs bonnes raisons de garder le proxy léger :

  • Performance — Le proxy s'exécute sur chaque requête correspondant au matcher, y compris les requêtes de prefetch. Une validation JWT ajouterait de la latence à chaque navigation.
  • Défense en profondeur — Suite au CVE-2025-29927, la communauté Next.js recommande de ne jamais se fier uniquement au proxy pour la sécurité. Validez toujours l'authentification au niveau de vos données.
  • Séparation des responsabilités — Le proxy gère le routage, vos Server Components gèrent la logique métier. C'est plus propre et beaucoup plus facile à tester.

Validation JWT dans un Server Component

Bon, concrètement, voici comment structurer la validation côté serveur avec la bibliothèque jose. C'est l'approche recommandée par l'équipe Next.js :

// lib/auth/session.ts
import { jwtVerify, SignJWT } from 'jose'
import { cookies } from 'next/headers'

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

export async function verifySession() {
  const cookieStore = await cookies()
  const token = cookieStore.get('session')?.value

  if (!token) {
    return null
  }

  try {
    const { payload } = await jwtVerify(token, secretKey, {
      algorithms: ['HS256'],
    })
    return {
      userId: payload.sub as string,
      role: payload.role as string,
      expiresAt: new Date(payload.exp! * 1000),
    }
  } catch {
    return null
  }
}

export async function createSession(userId: string, role: string) {
  const token = await new SignJWT({ sub: userId, role })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .setIssuedAt()
    .sign(secretKey)

  const cookieStore = await cookies()
  cookieStore.set('session', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7, // 7 jours
  })
}

Puis dans votre Server Component :

// app/dashboard/page.tsx
import { verifySession } from '@/lib/auth/session'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const session = await verifySession()

  if (!session) {
    redirect('/login')
  }

  return (
    

Bienvenue, utilisateur {session.userId}

Rôle : {session.role}

) }

Redirections et rewrites dans proxy.ts

Au-delà de l'authentification, le proxy est vraiment excellent pour les redirections d'URL et les rewrites. Voici les patterns les plus courants qu'on utilise au quotidien.

Redirections pour les anciennes URLs

Celui-là, c'est un classique. Vous refaites votre architecture d'URLs et vous devez rediriger les anciens chemins :

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const redirects: Record = {
  '/old-blog': '/blog',
  '/ancien-produit': '/produits',
  '/legacy-api': '/api/v2',
}

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

  // Vérifier les redirections
  for (const [from, to] of Object.entries(redirects)) {
    if (pathname.startsWith(from)) {
      const newPath = pathname.replace(from, to)
      return NextResponse.redirect(new URL(newPath, request.url), 301)
    }
  }

  return NextResponse.next()
}

Rewrites pour l'internationalisation

Les rewrites sont particulièrement utiles pour servir du contenu localisé sans changer l'URL visible. C'est un pattern qu'on voit de plus en plus dans les projets multilingues :

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const locales = ['fr', 'en', 'de', 'es']
const defaultLocale = 'fr'

function getLocaleFromHeaders(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language')
  if (!acceptLanguage) return defaultLocale

  for (const locale of locales) {
    if (acceptLanguage.includes(locale)) {
      return locale
    }
  }
  return defaultLocale
}

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

  // Ignorer les fichiers statiques et les APIs
  if (pathname.startsWith('/api') || pathname.includes('.')) {
    return NextResponse.next()
  }

  // Vérifier si un locale est déjà dans l'URL
  const hasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (!hasLocale) {
    const locale = request.cookies.get('NEXT_LOCALE')?.value
      || getLocaleFromHeaders(request)
    // Rewrite transparent : l'URL ne change pas pour l'utilisateur
    return NextResponse.rewrite(
      new URL(`/${locale}${pathname}`, request.url)
    )
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Headers de sécurité et rate limiting

Le proxy est l'endroit idéal pour injecter des headers de sécurité à toutes vos réponses. Voici un setup de base qui couvre l'essentiel :

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const response = NextResponse.next()

  // Headers de sécurité
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()',
  )
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains',
  )

  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Rate limiting basique pour les routes API

Pour les routes API, on peut implémenter un rate limiting simple directement dans le proxy. Un petit avertissement quand même : cette approche fonctionne pour un seul serveur. En production avec plusieurs instances, il faudra passer à une solution externe comme Redis.

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const rateLimit = new Map()
const WINDOW_MS = 60 * 1000 // 1 minute
const MAX_REQUESTS = 100

function checkRateLimit(ip: string): boolean {
  const now = Date.now()
  const entry = rateLimit.get(ip)

  if (!entry || now > entry.resetAt) {
    rateLimit.set(ip, { count: 1, resetAt: now + WINDOW_MS })
    return true
  }

  if (entry.count >= MAX_REQUESTS) {
    return false
  }

  entry.count++
  return true
}

export function proxy(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
    if (!checkRateLimit(ip)) {
      return NextResponse.json(
        { error: 'Trop de requêtes. Réessayez plus tard.' },
        { status: 429 }
      )
    }
  }

  return NextResponse.next()
}

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

Organiser un proxy complexe en modules

Un seul fichier proxy.ts est supporté par projet — c'est une contrainte. Mais rien ne vous empêche de découper votre logique en modules séparés pour garder le code maintenable. C'est d'ailleurs ce qu'on recommande dès que votre proxy dépasse 50-60 lignes.

// lib/proxy/auth.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const protectedRoutes = ['/dashboard', '/settings']

export function handleAuth(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  const isProtected = protectedRoutes.some((route) =>
    request.nextUrl.pathname.startsWith(route)
  )

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return null // Pas de réponse = passer au handler suivant
}
// lib/proxy/headers.ts
import { NextResponse } from 'next/server'

export function addSecurityHeaders(response: NextResponse) {
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  return response
}
// proxy.ts
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { handleAuth } from './lib/proxy/auth'
import { addSecurityHeaders } from './lib/proxy/headers'

export function proxy(request: NextRequest) {
  // 1. Vérifier l'authentification
  const authResponse = handleAuth(request)
  if (authResponse) return authResponse

  // 2. Ajouter les headers de sécurité
  const response = NextResponse.next()
  addSecurityHeaders(response)

  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Cette approche vous donne un fichier proxy.ts principal propre et lisible, avec toute la logique métier bien séparée dans des modules dédiés. Beaucoup plus agréable à maintenir sur le long terme.

Configuration avancée du matcher

Le matcher est votre outil principal pour contrôler quand le proxy s'exécute. Bien le configurer, c'est éviter pas mal de bugs subtils. Voici les patterns les plus utiles :

export const config = {
  matcher: [
    // Matcher simple : une seule route
    '/about',

    // Matcher avec wildcard : toutes les sous-routes
    '/dashboard/:path*',

    // Matcher avec regex négative : tout sauf les fichiers statiques
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',

    // Matcher multiple : plusieurs routes spécifiques
    '/api/:path*',
    '/dashboard/:path*',
    '/settings/:path*',
  ],
}

Le matcher supporte aussi une syntaxe objet plus avancée avec has et missing pour filtrer sur les headers, cookies ou query params. Ça peut être très pratique dans certains cas :

export const config = {
  matcher: [
    {
      source: '/api/:path*',
      has: [
        { type: 'header', key: 'x-api-key' },
      ],
    },
    {
      source: '/dashboard/:path*',
      missing: [
        { type: 'cookie', key: 'session' },
      ],
    },
  ],
}

Pièges courants et solutions

Après avoir migré plusieurs projets vers proxy.ts, voici les erreurs les plus fréquentes. Si vous pouvez en éviter ne serait-ce qu'une, ce guide aura déjà rempli son rôle.

1. Oublier de renommer la fonction

C'est le piège numéro un. Renommer le fichier ne suffit pas. Si vous gardez export function middleware() dans proxy.ts, Next.js vous affichera une erreur. La fonction doit s'appeler proxy (ou être un export par défaut).

2. Utiliser des packages incompatibles avec l'ancienne approche Edge

Bonne nouvelle ici : comme le proxy tourne maintenant sur Node.js, les packages qui nécessitaient des workarounds pour l'Edge Runtime fonctionnent directement. Vous pouvez retirer les shims et polyfills que vous aviez ajoutés — ça fait du bien de nettoyer ça.

3. Logique trop lourde dans le proxy

C'est probablement l'erreur la plus courante et la plus coûteuse en termes de performances. Le proxy s'exécute sur chaque requête correspondant au matcher — y compris les prefetch de navigation côté client. Si vous y mettez des requêtes base de données ou des appels API, votre application va devenir lente. Gardez-le rapide : lecture de cookies, vérification de présence, redirections. C'est tout.

4. Ignorer les requêtes de fichiers statiques

Sans matcher approprié, le proxy intercepte aussi les requêtes pour les images, les polices, les scripts. Utilisez toujours un matcher qui exclut au minimum _next/static, _next/image et favicon.ico. On a vu des projets où le proxy ralentissait le chargement de chaque image sans que personne ne s'en rende compte.

5. Se fier uniquement au proxy pour la sécurité

C'est la leçon la plus importante du CVE-2025-29927 : ne jamais utiliser le proxy comme seule couche de sécurité. Implémentez une défense en profondeur avec validation dans vos Server Components et votre Data Access Layer. Le proxy filtre, vos composants vérifient.

Foire aux questions

Est-ce que middleware.ts fonctionne encore dans Next.js 16 ?

Oui, middleware.ts est toujours fonctionnel dans Next.js 16 pour les cas d'usage nécessitant l'Edge Runtime. Cependant, il est officiellement déprécié et affichera un avertissement au build. Il sera complètement supprimé dans une future version majeure, donc autant migrer maintenant plutôt que de repousser l'inévitable.

Peut-on utiliser jsonwebtoken au lieu de jose dans proxy.ts ?

Techniquement oui, puisque proxy.ts tourne sur le runtime Node.js et a accès à toutes les APIs Node.js, y compris crypto. Mais la bibliothèque jose reste recommandée : elle est plus légère, utilise les Web Crypto APIs standards, et fonctionne de manière identique côté client et serveur. Si vous devez absolument utiliser jsonwebtoken, c'est désormais possible sans erreur runtime — mais honnêtement, ce n'est pas la meilleure pratique.

Comment tester proxy.ts en local ?

Le proxy s'exécute automatiquement avec next dev, donc pas de config spéciale. Pour tester des cas spécifiques comme les redirections ou le rate limiting, vous pouvez utiliser curl depuis le terminal, ou des outils comme Thunder Client dans VS Code. Pour des tests unitaires, extrayez votre logique dans des modules séparés (comme montré plus haut) et testez ces modules individuellement avec Jest ou Vitest.

Le proxy affecte-t-il les performances de mon application ?

Ça dépend entièrement de ce que vous y mettez. Un proxy léger (vérification de cookie, redirections simples) ajoute généralement moins de 5 ms de latence — c'est imperceptible. En revanche, des opérations lourdes comme des requêtes base de données ou de la validation JWT complète peuvent ajouter des centaines de millisecondes. La règle d'or : si une opération prend plus de 10 ms, elle ne devrait probablement pas être dans le proxy.

Faut-il mettre à jour NextAuth / Auth.js pour proxy.ts ?

Si vous utilisez NextAuth.js (Auth.js), vérifiez que vous utilisez la version 5.x ou supérieure, qui prend en charge le nouveau fichier proxy.ts. Les versions antérieures référençaient explicitement middleware.ts dans leur configuration. Consultez la documentation officielle d'Auth.js pour les instructions de migration spécifiques à votre version.

À propos de l'auteur Editorial Team

Our team of expert writers and editors.