Migrating from Middleware to Proxy in Next.js 16

Step 22 of 31Next.js Tutorial Series | Source code for this step

Live Demo →


What You Will Build

↑ Index

If you run pnpm dev (or npm run dev) with Next.js 16, you'll see this warning:

⚠ The "middleware" file convention is deprecated.
  Please use "proxy" instead.
  Learn more: https://nextjs.org/docs/messages/middleware-to-proxy

In Step 15, we created middleware.ts to protect routes using NextAuth.js. That file convention is now deprecated in Next.js 16. This step follows the official migration guide. You will:

  1. Understand what Proxy is and how it differs from Middleware
  2. Learn why Next.js renamed the convention
  3. Migrate your middleware.ts to proxy.ts
  4. Verify the deprecation warning is gone and route protection still works

Table of Contents

  1. What Is Proxy?
  2. Why Did Next.js Rename Middleware to Proxy?
  3. The Proxy Concept in JavaScript
  4. Proxy vs Middleware — What Changed?
  5. Migrating Our App
  6. Understanding the Migrated Code
  7. Verify the Migration
  8. Summary & Key Takeaways

What Is Proxy?

↑ Index

A proxy is code that sits between the client and your application. When a browser makes a request, the proxy intercepts it before any page or API route runs. It can then decide what to do:

  • Allow the request to continue normally
  • Redirect to a different URL (e.g., send unauthenticated users to /login)
  • Rewrite the request to a different route (e.g., serve /about-v2 when the user visits /about)
  • Modify headers on the request or response
  • Respond directly without ever hitting the application
Browser request
     │
     ▼
  ┌────────┐
  │ Proxy  │ ← runs BEFORE any page or API route
  └────┬───┘
       │
       ├── redirect → /login
       ├── rewrite → /different-page
       ├── modify headers
       └── allow → continue to the page/route

This is exactly what our middleware.ts was doing in Step 15 — intercepting requests to /posts/new and redirecting unauthenticated users to /login. The functionality is identical; only the file convention name has changed.

How Proxy Runs

Proxy runs on the server, before routes are rendered. In production on platforms like Vercel, it runs at the Edge — a lightweight environment close to the user, separate from the main application server.

This is why our auth config was split into two files in Step 15:

FilePurposeRuntime
auth.config.tsLightweight config (session, callbacks)Edge-safe
auth.tsFull config (bcrypt, Prisma, providers)Node.js only

The proxy can only import auth.config.ts because the Edge Runtime does not support Node.js modules like bcrypt or prisma. This has not changed — the same restriction applies to proxy.ts.


Why Did Next.js Rename Middleware to Proxy?

↑ Index

The Next.js team renamed the convention for two reasons:

1. The word "middleware" was misleading

"Middleware" in Express.js means a function that runs in the middle of a request pipeline — you can chain many middlewares, each modifying the request. Developers coming from Express expected Next.js middleware to work the same way.

But Next.js middleware is fundamentally different. It's not a chain of functions processing a request through your app. It's a single function that sits in front of your app — intercepting requests before they reach any route. That's what a proxy does.

2. Middleware was being overused

Because "middleware" sounds general-purpose, developers were putting too much logic in it — database queries, heavy computations, complex business logic. Next.js middleware was never designed for that. It's meant for lightweight operations: checking a cookie, redirecting, setting a header.

The rename to "proxy" signals this intent: use it as a thin network layer, not as a place for application logic.

In practice: The Next.js team's recommendation is to avoid relying on proxy unless no other option exists. For most tasks, there are better APIs — Server Components, Server Actions, auth() checks in pages, etc. Proxy is a last resort for things that must happen before the route renders.


The Proxy Concept in JavaScript

↑ Index

For a complete tutorial on JavaScript's built-in Proxy, see JavaScript Proxy: Intercept and Control Object Operations.

The name "proxy" didn't come out of nowhere. JavaScript itself has a built-in Proxy object that works on the same principle: an intermediary that intercepts operations before they reach the real target.

JavaScript's new Proxy()

new Proxy(target, handler) creates a wrapper around any object or function. You define traps in the handler — functions that intercept specific operations like reading, writing, or deleting properties.

const user = { name: "John", role: "admin" };

const proxy = new Proxy(user, {
  get(target, property) {
    console.log(`Accessing ${property}`);
    return target[property];
  },
});

console.log(proxy.name);
// Output:
// Accessing name
// John

The caller interacts with proxy as if it were the real user object — but the get trap intercepts every property access.

Validation with set Trap

const person = { age: 20 };

const proxy = new Proxy(person, {
  set(target, property, value) {
    if (property === "age" && value < 0) {
      throw new Error("Age cannot be negative");
    }
    target[property] = value;
    return true;
  },
});

proxy.age = 25; // ✅ works
proxy.age = -5; // ❌ throws Error

The proxy enforces rules before the value reaches the real object.

Access Control

const user = { name: "Admin", password: "secret123" };

const proxy = new Proxy(user, {
  get(target, prop) {
    if (prop === "password") {
      return "Access denied";
    }
    return target[prop];
  },
});

console.log(proxy.password); // "Access denied"
console.log(proxy.name);     // "Admin"

Common Proxy Traps

TrapTriggered when
getReading a property
setWriting a property
hasUsing the in operator
deletePropertyDeleting a property
applyCalling a function
constructUsing new
ownKeysListing keys (Object.keys)

Real-World Usage

JavaScript's Proxy powers reactivity systems in frameworks like Vue 3. When you write const state = reactive({ count: 0 }), Vue internally wraps the object in a Proxy — so it knows when data changes and can update the UI automatically.

The Mental Model

Caller → Proxy → Real Object

The proxy decides what happens before the real object is touched. This is the same mental model for both:

JavaScript new Proxy()Next.js proxy.ts
What it interceptsObject/function operations (get, set, delete)HTTP requests (before routes render)
Where it sitsBetween the caller and the target objectBetween the browser and the application
What it can doValidate, log, deny, modifyRedirect, rewrite, set headers, deny
Core ideaAn intermediary that controls accessAn intermediary that controls access

Important: Next.js's proxy.ts does not use JavaScript's new Proxy() internally. They are different implementations that share the same concept and name. Next.js borrowed the term because the behavior is analogous — intercepting operations before they reach the real target.


Proxy vs Middleware — What Changed?

↑ Index

The API is nearly identical. Here's what changed and what stayed the same:

What changed

Before (deprecated)After
File: middleware.tsFile: proxy.ts
Export: export function middleware() or export const middleware = ...Export: export function proxy() or export default function proxy()

What stayed the same

  • File location — project root (next to app/), or inside src/ if using that structure
  • config.matcher — same syntax, same patterns
  • NextRequest and NextResponse — same API
  • Edge Runtime restrictions — same limitations (no Node.js modules)
  • Execution order — still runs before routes are rendered

The migration is essentially: rename the file, rename the export.


Migrating Our App

↑ Index

Here is our current middleware.ts from Step 15:

// middleware.ts (DEPRECATED)
import NextAuth from 'next-auth'
import { authConfig } from './auth.config'

const { auth } = NextAuth(authConfig)

export const middleware = auth

export const config = {
  matcher: ['/posts/new'],
}

Step 1 — Rename the file

Rename middleware.ts to proxy.ts:

mv middleware.ts proxy.ts

Step 2 — Update the export

Replace the contents of proxy.ts:

// proxy.ts
import NextAuth from 'next-auth'
import { authConfig } from './auth.config'

const { auth } = NextAuth(authConfig)

export default auth

Step 3 — Add the matcher config

The config.matcher works the same way. Add it back:

// proxy.ts
import NextAuth from 'next-auth'
import { authConfig } from './auth.config'

const { auth } = NextAuth(authConfig)

export default auth

export const config = {
  matcher: ['/posts/new'],
}

That's it. The complete migrated file is just 9 lines.

What about auth.config.ts and auth.ts?

No changes needed. The auth config split we set up in Step 15 still serves the same purpose — keeping the Edge-safe config (auth.config.ts) separate from the Node.js-only config (auth.ts). The proxy imports only auth.config.ts, just like the middleware did.


Understanding the Migrated Code

↑ Index

export default auth vs export const middleware = auth

In the old convention, Next.js looked for a named export called middleware. In the new convention, it accepts either:

  • A named export called proxy: export function proxy(request) { ... }
  • A default export: export default function proxy(request) { ... }

Since NextAuth's auth function is already a complete request handler (it checks the session, runs the authorized callback, and redirects if needed), we export it as the default export. Next.js picks it up and runs it as the proxy function.

The request flow (unchanged)

The behavior is identical to Step 15:

1. Browser requests /posts/new

2. Proxy runs:
   ├── auth() reads the JWT session cookie
   ├── Runs the `authorized` callback from auth.config.ts
   │     └── return !!auth?.user
   │
   ├── If user exists → allow request → page renders
   └── If no user    → redirect to /login

Where the file lives

project-root/
├── app/
│   └── ...
├── auth.config.ts    ← Edge-safe auth config (unchanged)
├── auth.ts           ← Full auth config (unchanged)
├── proxy.ts          ← Renamed from middleware.ts
└── ...

The file must be at the project root (same level as app/). This has not changed.


Verify the Migration

↑ Index

1. The deprecation warning is gone

Restart your dev server:

pnpm dev

The warning ⚠ The "middleware" file convention is deprecated should no longer appear.

2. Route protection still works

Test without a session:

  1. Open the app in an incognito/private browser window (no session)
  2. Navigate to /posts/new
  3. You should be redirected to /login — the proxy blocked the request

Test with a session:

  1. Log in with a valid account
  2. Navigate to /posts/new
  3. The page should load normally — the proxy allowed the request

The behavior is identical to Step 15. The only difference is the file name.


Summary & Key Takeaways

↑ Index

ConceptDetails
What is ProxyCode that runs between the client and your app — before any page or API route executes
Why the rename"Middleware" was confused with Express.js middleware and encouraged overuse; "Proxy" clarifies its purpose as a thin network layer
What changedFile name (middleware.tsproxy.ts) and export name (middlewareproxy or default export)
What stayed the sameFile location, config.matcher, NextRequest/NextResponse API, Edge Runtime restrictions
Migration stepsRename file, change export — everything else (auth config split, matcher, callbacks) stays identical
Auth config splitauth.config.ts (Edge-safe) and auth.ts (Node.js) — same pattern as Step 15, still required
Best practiceUse proxy as a last resort; prefer auth() checks in Server Components or Server Actions for most cases

Automated migration: Next.js provides a codemod that handles the rename automatically. You can run npx @next/codemod@canary middleware-to-proxy . instead of doing it manually. We did it manually in this tutorial so you understand what changed and why.

What is Next

In Step 23, we will add error handling using the error.tsx and not-found.tsx file conventions — showing meaningful error pages and custom 404 screens instead of crashing the app.