If you search for Next.js middleware, you will find years of examples that all look familiar:
middleware.tsBut in Next.js 16, that convention was renamed to:
proxy.tsThe 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 callerThis 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 fit | Why |
|---|---|---|
| Redirect a visitor before a page renders | Proxy | It can inspect the request and decide early. |
| Define a simple permanent redirect | next.config.ts | No custom runtime logic needed. |
| Render protected data | Server Component + data layer | Authorization stays close to the data. |
| Save a form inside your app | Server Action | The mutation belongs to your UI workflow. |
| Receive a webhook or expose JSON | Route Handler | Another system needs a stable HTTP contract. |
| Check a user's permission to delete a record | Server Action / Route Handler + data layer | The sensitive mutation needs its own authorization. |
| Choose a locale or experiment before routing | Proxy | It 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:
- The runtime work is limited to the requests that need it.
- Future teammates can see which part of the app the rule affects.
- 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:
- Rename
middleware.tstoproxy.ts. - Rename the exported
middlewarefunction toproxy. - Check every matcher. Remove broad patterns that do not need request-level logic.
- Move static redirects to
next.config.tswhere possible. - Move database-backed authorization to the functions that read or change protected data.
- Test direct requests to Route Handlers and direct Server Action paths; do not assume the browser redirect is the only guard.
- 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.
Discussion
Responses
No approved responses yet.