Article

Next.js Proxy vs Middleware in 2026: Auth, Redirects, and What Not to Put There

Next.js 16 renamed Middleware to Proxy. Learn when proxy.ts is the right place for redirects and lightweight request checks, when it is not, and how it fits with Route Handlers, Server Actions, and real authorization.

If you search for Next.js middleware, you will find years of examples that all look familiar:

middleware.ts

But in Next.js 16, that convention was renamed to:

proxy.ts

The underlying capability is familiar. The name is the useful part.

Calling it Proxy makes its actual job clearer: it sits in front of your app and can make a quick decision about an incoming request before the request reaches a page or endpoint.

That makes it useful. It also makes it very easy to misuse.

I use Proxy for a small number of request-level concerns: redirects that depend on the request, locale routing, lightweight access gates, experiments, and headers. I do not use it as the place where all application logic goes.

This is the practical version of that rule: what changed, what belongs in proxy.ts, and when a Route Handler, Server Action, page, or data-access layer is the better boundary.

The short answer

Use Proxy when you need a fast decision before a request reaches your application.

Good examples:

  • redirect an unauthenticated visitor away from /dashboard
  • redirect an old URL when the destination depends on the request
  • add or inspect request and response headers
  • route a request based on locale, device, or an experiment cookie
  • block obviously invalid traffic before it reaches an endpoint

Do not use Proxy for:

  • database reads on every request
  • full session validation or permission checks
  • your main business logic
  • form mutations
  • webhooks or public JSON APIs
  • a static list of redirects that can live in next.config.ts

The most important distinction is this:

Proxy can provide an optimistic gate. Your server code must still enforce authorization where the data is read or changed.

Middleware is now Proxy

Starting with Next.js 16, middleware.ts and the exported middleware function were renamed to proxy.ts and proxy.

The migration is small:

// Before: middleware.ts
export function middleware(request: Request) {
  // ...
}

// After: proxy.ts
export function proxy(request: Request) {
  // ...
}

For a codebase that already uses Middleware, the rename alone does not require you to redesign the feature. But it is a good opportunity to inspect what the file has gradually become.

In many projects, a tiny redirect file turns into a place that fetches a user, loads a subscription, checks a feature flag, calls an API, and decides whether to show half the application. That is usually a sign that the boundary is doing too much.

The name Proxy is a helpful reminder: this is request routing at the edge of your app, not an Express-style chain for every piece of server logic.

What Proxy is actually good at

Proxy runs before a request is completed. It can redirect, rewrite, modify headers, or return a response directly.

That is a narrow responsibility, but a valuable one.

1. Lightweight route protection

The most common use case is keeping a visitor without a session marker out of a private part of the app.

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

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

  if (!session) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', request.nextUrl.pathname)

    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

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

This gives the user a good experience: someone who is clearly not signed in is sent to login before the dashboard begins rendering.

But this is not enough to secure the dashboard by itself.

A cookie can be stale. A request may reach your data layer another way. A Server Action or Route Handler can be called independently of the browser page. The code that fetches or mutates protected data must verify the session and the user's permissions too.

Think of Proxy as the person at the building entrance checking whether someone has a badge. The room containing customer data still needs a lock.

2. Request-aware redirects

For a simple permanent redirect, use redirects in next.config.ts. It is clearer, more declarative, and does not run application code for every match.

Proxy becomes useful when the redirect depends on request data.

For example, this sends a visitor with an older campaign cookie to a new landing page:

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

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

  if (request.nextUrl.pathname === '/pricing' && campaign === 'startup-2025') {
    return NextResponse.redirect(new URL('/pricing/startup', request.url))
  }

  return NextResponse.next()
}

The rule of thumb is simple:

  • known URL-to-URL redirect → next.config.ts
  • redirect that needs the incoming request → Proxy
  • redirect after a user submits something → Server Action or Route Handler

3. Locale and experiment routing

Proxy is a natural place for routing concerns that should happen before a page is selected.

For example, an app may redirect a new visitor to a locale-prefixed route based on an Accept-Language header, or assign a harmless UI experiment based on a cookie.

Keep the decision lightweight and deterministic. The request should not need to wait for several remote services just to learn which homepage it should see.

4. Small header changes

You can also set headers for a group of pages.

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

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

  response.headers.set('x-page-group', 'marketing')

  return response
}

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

That said, do not introduce a Proxy only to add a static header if the same policy belongs in your hosting configuration or next.config.ts. A focused proxy.ts is much easier to reason about six months later.

What does not belong in Proxy

The biggest Proxy problems are architectural, not syntactic.

Do not make a database call for every request

It is tempting to do this:

// Avoid this as a general pattern
export async function proxy(request: NextRequest) {
  const user = await db.user.findUnique({
    where: { sessionToken: request.cookies.get('session')?.value },
  })

  // Decide whether this request can continue
}

Now every matching navigation, prefetch, image-adjacent request, and retry can carry a database cost. It also makes the whole application depend on the latency and availability of that query before it can even start rendering.

If the check must be exact, do it close to the protected data or mutation. If Proxy can make a cheap optimistic decision first, use it for that and no more.

Do not treat it as complete authorization

Authorization answers questions such as:

  • Can this user read this project?
  • Can this user delete this invoice?
  • Is this role allowed to update a team member?

Those questions belong in a data-access function, service, Server Action, or Route Handler that performs the sensitive work.

Here is the shape I aim for:

Proxy             → redirect obviously unauthenticated visitors
Server Component  → load page data through an authorized query
Server Action     → re-check user and permissions before a mutation
Route Handler     → authenticate and authorize an external HTTP caller

This can feel repetitive at first. It is deliberate. Security should not rely on one routing convention being reached in exactly one way.

Do not put mutations there

Creating a record, charging a card, sending an email, or updating a profile is not a request-routing decision.

For an action initiated inside your Next.js UI, use a Server Action. For an HTTP endpoint called by Stripe, a mobile app, or another service, use a Route Handler. If you are choosing between those two, see Server Actions vs API Routes in Next.js: the rules I actually use.

Proxy vs Route Handler vs Server Action

These features can all run server-side, but they solve different problems.

You need to…Best fitWhy
Redirect a visitor before a page rendersProxyIt can inspect the request and decide early.
Define a simple permanent redirectnext.config.tsNo custom runtime logic needed.
Render protected dataServer Component + data layerAuthorization stays close to the data.
Save a form inside your appServer ActionThe mutation belongs to your UI workflow.
Receive a webhook or expose JSONRoute HandlerAnother system needs a stable HTTP contract.
Check a user's permission to delete a recordServer Action / Route Handler + data layerThe sensitive mutation needs its own authorization.
Choose a locale or experiment before routingProxyIt is an early request-level routing concern.

That last column matters more than the API names. Start by naming the job, then choose the smallest boundary that does it well.

Keep matchers narrow

A Proxy that runs for the whole application is easy to write:

export function proxy() {
  return NextResponse.next()
}

It is not always a good default.

Use matcher to state exactly where your rule matters:

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

Narrow matchers make three things better:

  1. The runtime work is limited to the requests that need it.
  2. Future teammates can see which part of the app the rule affects.
  3. You are less likely to accidentally alter assets, public pages, or endpoints that should remain untouched.

If a rule needs exceptions, make those explicit. A readable matcher is better than a clever global condition that everyone is afraid to change.

A practical migration checklist

If you have a middleware.ts file in an older project, this is the short list I would use before upgrading:

  1. Rename middleware.ts to proxy.ts.
  2. Rename the exported middleware function to proxy.
  3. Check every matcher. Remove broad patterns that do not need request-level logic.
  4. Move static redirects to next.config.ts where possible.
  5. Move database-backed authorization to the functions that read or change protected data.
  6. Test direct requests to Route Handlers and direct Server Action paths; do not assume the browser redirect is the only guard.
  7. Test logged-out, expired-session, and no-permission cases separately.

Next.js also provides a codemod for the mechanical rename. The useful part is not merely getting the new filename to compile. It is ending with a small, predictable Proxy that makes request routing easier to understand.

The rule I actually use

When I am considering proxy.ts, I ask one question:

Does this need to happen before the application chooses or renders the response?

If yes, Proxy may be the right fit.

If the question is instead about reading data, changing data, proving a user's permission, or exposing a backend capability, I choose another server boundary.

That keeps the implementation honest:

  • Proxy is for early request decisions.
  • Server Components are for rendering with server-side data.
  • Server Actions are for internal UI mutations.
  • Route Handlers are for HTTP interfaces.
  • Your data layer is where authorization is enforced for real.

The rename from Middleware to Proxy is not just terminology. It is a useful architecture hint. Treat the file like a small checkpoint in front of your app, and it will stay fast, understandable, and much harder to accidentally turn into your entire backend.

FAQ

Is Next.js Middleware removed in Next.js 16?

No. The convention was renamed to Proxy in Next.js 16. Existing Middleware code can be migrated to proxy.ts, but the functionality is not simply gone.

Should I use Proxy for authentication in Next.js?

Use it for a lightweight, optimistic redirect when a visitor clearly has no session. Still validate the session and permissions in the server code that reads or changes protected data.

Should a Proxy query my database?

Usually no. A database request in Proxy adds work and latency before the app can render. Put exact authorization checks close to the protected query or mutation instead.

When should I use a Route Handler instead of Proxy?

Use a Route Handler when you need a real HTTP endpoint: webhooks, public APIs, mobile clients, callbacks, custom JSON responses, files, or streaming. Proxy is for routing decisions that happen before those endpoints run.

Share this post

Send it to someone who might find it useful.

Discussion

Responses

0 responses

No approved responses yet.

Response

Join the discussion

Moderated
Guest responseGuestReviewed before publishing