In Next.js 16, the middleware.ts file convention is deprecated and renamed to proxy.ts, and the exported function is renamed from middleware to proxy. To migrate safely, run the official codemod, tighten the matcher, remove any runtime config because Proxy always runs on Node.js, rename the related config flags, and verify authorization inside each Server Function instead of trusting Proxy alone.
The rename looks cosmetic, but it changes what the file is for. The Next.js team now describes Proxy as a network boundary in front of your app and recommends it as a last resort. For AI-assisted codebases, where an assistant often stacks auth, logging, redirects, and A/B logic into one middleware file, this is the right moment to audit what that file actually does.
Next.js 16 middleware to proxy: 6 steps at a glance
| Step | What changes | What to verify |
|---|---|---|
| 1. Run the codemod | File and function renamed | Only one proxy.ts, one exported function |
| 2. Rename config flags | skipMiddlewareUrlNormalize becomes skipProxyUrlNormalize | next.config has no old flag names |
| 3. Drop the runtime setting | Proxy always uses Node.js | No runtime export, build does not throw |
| 4. Tighten the matcher | Without a matcher, Proxy runs on every request | Static assets and images are excluded |
| 5. Move auth checks closer to data | Server Functions are not separate routes | Every action checks the session itself |
| 6. Test and deploy | Matcher and routing logic covered by tests | No stale middleware warnings after deploy |
Why was middleware renamed to proxy in Next.js 16?
Next.js renamed middleware to proxy because the old term was confused with Express.js middleware and encouraged overuse. According to the official docs, "proxy" better describes a layer that runs at the network boundary and can handle requests before they reach your app, and the team wants developers to reach for it only when no better API exists.
The version history is short: Proxy arrived in v16.0.0, and the Node.js runtime for middleware became stable in v15.5.0. If you are already on Next.js 16 with Cache Components, the same upgrade pass is a good time to move this file, see Next.js 16 Cache Components: 5 migration steps.
1. Run the codemod and rename the function
Run the official codemod from the project root. It renames the file and the exported function, which is the whole mechanical change.
npx @next/codemod@canary middleware-to-proxy .
The result should look like this:
// proxy.ts (project root or src/, next to app/)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}
The file must export a single function, either named proxy or as the default export. Multiple proxy functions in one file are not supported, so split any helper logic into plain modules and call it from one entry point. If you customized pageExtensions, the file name must follow it, for example proxy.page.ts.
Done when: the dev server no longer prints the "middleware file convention is deprecated" warning and no middleware.ts remains.
2. Rename the related config flags
Flags that contained the word middleware were renamed too. skipMiddlewareUrlNormalize is now skipProxyUrlNormalize. The codemod may not catch every config file, so search manually.
grep -rn "skipMiddlewareUrlNormalize\|middleware" next.config.* package.json
skipTrailingSlashRedirect keeps its name. Use skipProxyUrlNormalize only if you do custom rewrites with fetch() and need the original URL shape and RSC headers.
3. Remove the runtime setting: Proxy always runs on Node.js
Proxy runs on the Node.js runtime and this cannot be configured. Setting the runtime option inside a Proxy file throws an error, so delete any export const runtime = 'edge' you carried over.
The upside is that standard Node.js libraries work, including crypto and JWT libraries such as jose, with no edge-specific workarounds. The downside is that code which depended on edge-only behavior needs a review. Also remember that Proxy is meant to run separately from your render code, so do not rely on shared modules or globals to pass state. Use headers, cookies, rewrites, redirects, or the URL instead.
4. Tighten the matcher before anything else
Without a matcher, Proxy runs on every request, including _next/static, _next/image, and files in public/. Auth redirects can then block CSS, JavaScript, or images, which usually shows up only in staging.
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
}
Three rules prevent most matcher bugs:
- Constants only. Matcher values must be static so they can be analyzed at build time. Variables are ignored.
- Start with a slash. Every
sourcepattern must begin with/, and patterns are anchored to the start of the path. - Do not exclude
_next/dataon purpose. Proxy still runs for_next/dataroutes even when a negative matcher excludes them, which is intentional to avoid protecting a page but forgetting its data route.
Remember the execution order too: config headers, then redirects, then Proxy, then beforeFiles rewrites, then filesystem routes. If a redirect in next.config and a Proxy redirect disagree, the config one wins because it runs first.
5. Do not rely on Proxy alone for authentication
Server Functions are not separate routes. They are handled as POST requests to the route where they are used, so a matcher that excludes a path also skips Server Function calls on that path. A refactor that moves a Server Function to another route can silently remove Proxy coverage.
Use Proxy for optimistic checks such as redirecting anonymous users away from /dashboard, and verify the session again next to the data:
'use server'
import { verifySession } from '@/lib/dal'
export async function deleteProject(id: string) {
const session = await verifySession() // throws or redirects if invalid
// authorize against session.userId before touching the database
}
This matters most in AI-generated code, where assistants often place a single auth check in middleware and assume every route is covered. Treat that as a finding in any AI-generated codebase audit.
6. Test the matcher and deploy
Next.js ships experimental helpers in next/experimental/testing/server. Use unstable_doesProxyMatch to assert that Proxy runs only where you expect, and call proxy() directly with a NextRequest to check rewrites and redirects with isRewrite, getRewrittenUrl, and getRedirectUrl.
import { unstable_doesProxyMatch } from 'next/experimental/testing/server'
import { config } from './proxy'
expect(unstable_doesProxyMatch({ config, url: '/_next/static/app.js' })).toEqual(false)
expect(unstable_doesProxyMatch({ config, url: '/dashboard' })).toEqual(true)
Static export is not supported for Proxy, while Node.js servers and Docker are. On Vercel, older CLI versions printed a warning about missing legacy middleware.ts after the rename, which Vercel fixed in a CLI update, so a current build should deploy cleanly. After deploying, request a protected route, a public route, and one static asset, and confirm each behaves as expected.
Common mistakes during the migration
- Renaming only the file. The exported function must be
proxy(or a default export). A leftovermiddlewareexport does nothing. - Forgetting the matcher. Proxy then runs on every asset request and adds latency to each one.
- Keeping edge assumptions. Do not set
runtime, and remove polyfills that existed only for the edge runtime. - Putting heavy work in Proxy. Database queries and large header payloads belong elsewhere. Oversized headers can trigger 431 errors.
- Treating Proxy as the only gate. Always re-check authorization in Server Functions and data access code.
FAQ
Is middleware.ts removed in Next.js 16?
No, it is deprecated and renamed. The convention still exists but Next.js warns that you should use proxy, and the Edge runtime case is the main reason to keep the old file for now.
Can I still use the Edge runtime with proxy.ts?
No. Proxy runs on the Node.js runtime and the runtime option is not available. Setting it throws an error.
Does proxy.ts run on every request?
Only if you omit the matcher. Without one, it runs on all requests, including static files and images, so always define a matcher.
Is Proxy enough to protect my routes?
No. Use it for optimistic checks and verify authorization again inside Server Functions and your data access layer.
Does the codemod change my matcher or logic?
No. It renames the file and function only. Review the matcher, config flags, and runtime settings yourself.
If you are upgrading an inherited or AI-generated Next.js app and want a second pair of eyes on routing, auth, and caching, you can hire me as a freelance full-stack developer for a focused upgrade audit.
Sources: Next.js docs: proxy.js, Next.js: Renaming Middleware to Proxy, Vercel Community: legacy middleware warning.