Guide for Next.js 16 proxy patterns, replacing the deprecated middleware functionality. Covers the new proxy.ts file convention, async request APIs, and proper request interception...
In Next.js 16, the middleware file convention has been renamed to proxy to clarify network boundary and routing focus. This change affects how you intercept and modify requests before they reach your application.
β οΈ CRITICAL: This is a breaking change in Next.js 16:
middleware.ts β proxy.tsexport function middleware() β export function proxy()nodejs runtime (not edge runtime)skipMiddlewareUrlNormalize β skipProxyUrlNormalizeUse this skill when:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Authentication check
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function proxy(request: NextRequest) {
// Next.js 16: All dynamic APIs are now async
// Authentication check
const cookieStore = await import('next/headers').then(m => m.cookies());
const token = cookieStore.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// Public routes that don't require authentication
const isPublicRoute =
pathname === '/login' ||
pathname === '/register' ||
pathname.startsWith('/api/auth')
// Protected routes that require authentication
const isProtectedRoute =
pathname.startsWith('/dashboard') ||
pathname.startsWith('/profile') ||
pathname.startsWith('/api/private')
if (isProtectedRoute && !isPublicRoute) {
// In Next.js 16, access cookies asynchronously
const cookieStore = await import('next/headers').then(m => m.cookies());
const token = cookieStore.get('auth-token')
if (!token) {
// Redirect to login page
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
}
// Continue with the request
return NextResponse.next()
}
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).*)',
],
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
skipMiddlewareUrlNormalize: true,
}
module.exports = nextConfig
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
skipProxyUrlNormalize: true,
}
module.exports = nextConfig
| Next.js 15 Middleware | Next.js 16 Proxy |
|---|---|
| Runs in Edge Runtime | Runs in Node.js Runtime |
File: middleware.ts |
File: proxy.ts |
Function: middleware() |
Function: proxy() |
Config: skipMiddlewareUrlNormalize |
Config: skipProxyUrlNormalize |
| Limited request data access | Full Node.js capabilities |
When using the proxy pattern in Next.js 16, keep in mind that all dynamic APIs now return promises:
// β
Correct in Next.js 16
export async function proxy(request: NextRequest) {
const headersList = await import('next/headers').then(m => m.headers());
const userAgent = headersList.get('user-agent');
// Modify response headers
const response = NextResponse.next();
response.headers.set('x-custom-header', 'proxy-value');
return response;
}