Back to Blog
Goodbye middleware.ts, Hello proxy.ts: The Next.js 16 Migration Guide
Next.js Mastery

Goodbye middleware.ts, Hello proxy.ts: The Next.js 16 Migration Guide

Next.js 16 has killed `middleware.ts`. Learn how to migrate to `proxy.ts`, why auth in middleware is now considered unsafe, and how to master the new Node.js-based proxy layer.

December 20, 20254 min readRabi
nextjs 16proxy.tsweb-developmentreactsecuritybreaking-changes

If you just upgraded to Next.js 16 and your app exploded, you are not alone.

The most controversial breaking change in this release is the complete removal of middleware.ts. It has been replaced by a new primitive: proxy.ts.

And it’s not just a rename. The rules have changed.

Middleware to Proxy Migration

Why did they kill Middleware?

For years, developers abused Middleware. We engineered complex authentication flows, database calls, and heavy logic into a layer that was only meant for routing.

This came to a head with CVE-2025-29927, a vulnerability where Middleware authentication could be bypassed under high load due to Edge Runtime limitations.

Vercel's response in Next.js 16 is clear: Network boundaries must be explicit.

  • proxy.ts runs on Node.js (by default), not the limited Edge Runtime.
  • It is strictly for Routing (Rewrites, Redirects, Headers).
  • It is NOT for Authentication (Auth should happen in Layouts or Route Handlers).

The Migration: middleware.ts vs proxy.ts

1. File Rename & Location

  • Old: middleware.ts in root or src/.
  • New: app/proxy.ts (Must be inside the App Router directory).

2. Syntax Changes

The signature looks similar, but notice the function name and runtime behavior.

The Legacy Way (middleware.ts):

// ❌ middleware.ts (Legacy)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // We used to do Auth here... dangerous!
  const token = request.cookies.get('token')
  if (!token) return NextResponse.redirect(new URL('/login', request.url))
  
  return NextResponse.next()
}

The Next.js 16 Way (proxy.ts):

// βœ… app/proxy.ts
import { ProxyRequest, ProxyResponse } from 'next/server'
 
// Note: It's named 'proxy', not 'middleware'
export async function proxy(request: ProxyRequest): Promise<ProxyResponse> {
  const { pathname } = request.nextUrl
  
  // 1. Rewrites (A/B Testing, Localization)
  if (pathname === '/about') {
     return ProxyResponse.rewrite(new URL('/fr/about', request.url))
  }
 
  // 2. Redirects (Legacy paths)
  if (pathname.startsWith('/old-blog')) {
      return ProxyResponse.redirect(new URL('/blog', request.url))
  }
 
  // 3. Headers (Security)
  // You can now access full Node.js APIs here if needed!
  const res = ProxyResponse.next()
  res.headers.set('X-Frame-Options', 'DENY')
  return res
}

[!WARNING] Do not attempt to read databases or verify complex JWTs in proxy.ts. While it runs on Node.js and technically can connect to a DB, blocking the request at the proxy level adds significant latency to your TTFB (Time To First Byte) for every single request.

Where does Auth go now?

If proxy.ts is just for routing, where do we protect our routes?

Next.js 16 introduces Server Layout Guards.

Instead of a global middleware file, you wrap your protected routes in a layout.tsx that performs the check. This leverages React Server Components (RSC) to handle security closer to the data.

// app/dashboard/layout.tsx
import { redirect } from 'next/navigation'
import { verifySession } from '@/lib/auth' // Your standard server-side auth
 
export default async function DashboardLayout({ children }) {
  const session = await verifySession()
  
  if (!session) {
    redirect('/login') // Server-side redirect
  }
 
  return (
    <section>
        {children}
    </section>
  )
}

Production Snippets for proxy.ts

Here are the safe, approved patterns for the new Proxy layer.

1. The Localizer (Geo-Routing)

Since proxy.ts has full Node.js access, you can use powerful libraries like maxmind directly if you want, but the standard request.geo (on Vercel) is still the fastest.

export function proxy(request: ProxyRequest) {
  const country = request.geo?.country || 'US'
  const locale = getLocaleFromCountry(country) // e.g., 'fr-FR'
  
  // Transparent rewrite
  return ProxyResponse.rewrite(new URL(`/${locale}${request.nextUrl.pathname}`, request.url))
}

2. Header Injection (CSP & Security)

export function proxy(request: ProxyRequest) {
  const nonce = crypto.randomUUID() // Valid Node.js crypto!
  
  const headers = new Headers(request.headers)
  headers.set('x-nonce', nonce)
 
  const response = ProxyResponse.next({
    request: { headers }
  })
  
  response.headers.set('Content-Security-Policy', `script-src 'self' 'nonce-${nonce}'`)
  return response
}

Summary

The death of middleware.ts is painful but necessary. It forces us to decouple Routing (Proxy) from Security (Layouts/RSC).

Checklist for migration:

  1. [ ] Rename middleware.ts to app/proxy.ts.
  2. [ ] Rename exported function to proxy.
  3. [ ] CRITICAL: Move all Authentication logic OUT of the proxy and into layout.tsx or Server Actions.
  4. [ ] Celebrate your faster, more secure app.

Happy coding, and welcome to the Next.js 16 era. πŸš€

References & Further Reading

For those who want to verify the details or read the full specs, here are the official sources:

Join the Community

Get the latest articles on system design, frontend & backend development, and emerging tech trends delivered straight to your inbox.

No spam. Unsubscribe at any time.

Liked the blog?

Share it with your friends and help them learn something new!