Next.js 16.1 Migration: Refactoring middleware.ts to proxy.ts (Without Breaking Auth)

In this article
- How to Migrate middleware.ts to proxy.ts in Next.js 16.1
- The "Why": It's Not Just a Rename, It's a Boundary
- Step 1: The Mechanical Migration
- Step 2: The "Where Does Auth Go?" Crisis
- Step 3: The Async Trap (Breaking Changes)
- Advanced: When Should You Actually Use Proxy?
- Summary Checklist
- Frequently Asked Questions
You ran npm install next@latest, typed the upgrade command, and watched your terminal explode. Or maybe you just saw the deprecation warning in Next.js 16.1 and felt that familiar pit in your stomach. It’s not just you. Next.js 16.1 isn't just an upgrade; it’s a paradigm shift.
The headline feature—renaming middleware.ts to proxy.ts—sounds cosmetic. It isn't. It is Vercel telling us, loudly, that we have been using Middleware wrong for years.
If you are staring at a broken build or arguing on Reddit about where your authentication logic belongs now that the framework is pushing it out of the edge layer, this guide is for you. We are going to bridge the gap between the dry official docs and the architectural reality of running a production app.
How to Migrate middleware.ts to proxy.ts in Next.js 16.1
Quick Answer: To migrate from middleware.ts to proxy.ts in Next.js 16.1, you can run the official codemod: npx @next/codemod@canary middleware-to-proxy. This automatically renames your file and updates the exported function name to proxy. Architecturally, Next.js 16.1 restricts proxy.ts to edge-level network tasks like redirects, rewrites, and header modifications. Heavy application logic, such as database checks or complex JWT validation, should be moved out of the proxy and into your Data Access Layer (DAL) or Route Handlers.
The "Why": It's Not Just a Rename, It's a Boundary
For years, middleware.ts was the Wild West. Developers treated it like an Express.js server, stuffing it with database calls, heavy session validation, and complex feature flagging. The problem? It ran on the Edge Runtime, where high-latency operations killed performance and third-party node libraries failed silently.
Next.js 16.1 rebrands this to Proxy to enforce a strict mental model: this file is a network gatekeeper, not a backend server.
The Critical Technical Shift:
proxy.ts now defaults to the Node.js runtime, not Edge. This stabilizes access to standard Node APIs but introduces a hard constraint: you cannot return response bodies (JSON/HTML) from the proxy. You can only redirect, rewrite, or modify headers.
Step 1: The Mechanical Migration
If you haven't run the codemod yet, here is the manual breakdown to ensure your configuration remains valid.
The File Rename and Export
Move your file from the root (or /src) directory. Next.js 16.1 enforces a single proxy file per project to prevent the "execution chain hell" of previous versions.
- Old:
middleware.tsexportingmiddleware - New:
proxy.tsexportingproxy
// proxy.ts (formerly middleware.ts)
import { NextRequest, NextResponse } from 'next/server';
// The export must be named 'proxy' or be the default export
export function proxy(request: NextRequest) {
// Logic remains similar, but focused on routing/headers
return NextResponse.next();
}
// Matchers remain unchanged
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};You can automate this step by running the official Vercel codemod script:
npx @next/codemod@canary middleware-to-proxyUpdate Configuration Flags
If you utilized advanced flags in next.config.js to normalize URLs, update them to the new convention: skipMiddlewareUrlNormalize is now skipProxyUrlNormalize.
Step 2: The "Where Does Auth Go?" Crisis
This is the #1 pain point on GitHub and Reddit right now. If you previously performed full session validation and database lookups in Middleware, your app will break under the new model.
The Next.js 16.1 philosophy is the "Thin Proxy" pattern:
- In
proxy.ts: Check for the existence of a session cookie. If missing, redirect to login. Do not verify the token's cryptographic signature or query the DB here if it's expensive. - In Server Components: Perform the secure, authoritative validation of the user's session and permissions.
Fixing the "Logout Loop" (Cookie Syncing)
A common bug in this migration is the "session logout" loop. If your proxy refreshes a session token (common with Supabase) but fails to pass the updated Set-Cookie header to the browser, the user stays logged out.
The Fix: You must create a mutable response object, apply cookies to it, and return that specific object.
// Correct Pattern for Cookie Syncing in proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function proxy(request: NextRequest) {
// 1. Create a response that inherits the request headers
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
// 2. Perform your auth check (e.g., Supabase)
// This step MUST modify the 'response' object we created above
const session = await supabase.auth.getUser();
// 3. CRITICAL: Return the modified response, NOT a new one
return response;
}Library-Specific Survival Guide
Auth.js (v5) Migration
For Auth.js (formerly NextAuth), you should centralize your configuration in auth.ts. In Next.js 16.1, you can import the auth helper directly into proxy.ts to handle lightweight session checks.
Here is the exact code pattern for proxy.ts with Auth.js v5:
// proxy.ts
import { auth } from "@/auth" // Your centralized config
export const proxy = auth((req) => {
// If the user is logged in, req.auth will differ from null
if (!req.auth && req.nextUrl.pathname !== "/login") {
const newUrl = new URL("/login", req.nextUrl.origin)
return Response.redirect(newUrl)
}
})
// Optionally config matchers to exclude static files
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}Clerk
Rename middleware.ts to proxy.ts. The clerkMiddleware() helper remains the standard entry point and fully supports the new naming convention. It handles the "thin proxy" logic for you by default.
Step 3: The Async Trap (Breaking Changes)
Next.js 16.1 enforces stricter React 19 rules. You can no longer access cookies(), headers(), or params synchronously in your Server Components. This is a React 19 requirement that caught many developers off guard.
- Broken (v15):
const cookieStore = cookies(); - Fixed (v16.1):
const cookieStore = await cookies();
You must manually audit every instance where you access request data. If you miss one, your build might pass, but your runtime will explode.
(We covered the performance implications of async headers in our No-Nonsense Guide to Fixing Web Vitals).
Advanced: When Should You Actually Use Proxy?
While Vercel wants us to keep the proxy thin, it remains the only place for high-speed network routing before the cache layer hits.
- A/B Testing: Rewrite requests to variant buckets based on cookies. This happens before the filesystem check, ensuring the user gets the correct cached version.
- Multi-Tenancy: Rewrite
subdomain.app.comto/tenant/subdomainpaths based on the Host header. - Massive Redirects: If you have 1,000+ redirects, don't put them in
next.config.js. Use a Bloom filter or Edge Config lookup inproxy.tsto handle them programmatically without bloating your bundle.
Summary Checklist
- Run the Codemod:
npx @next/codemod@canary middleware-to-proxy. - Audit Auth Logic: Ensure cookie writes in the proxy are actually returned to the client.
- Await Your APIs: Update all calls to
cookies(),headers(), andparamsto be async. - Verify Runtime: Remember
proxy.tsdefaults to Node.js. If you specifically need Edge, you must verify your dependencies are compatible, though Node is now the preferred path.
The shift to proxy.ts in Next.js 16.1 is annoying today, but it forces a cleaner architecture. By treating the proxy as a strict network boundary rather than a "backend-lite," you align with the framework's caching model, ensuring faster cold starts and a more resilient authentication flow.
Frequently Asked Questions
Why was middleware.ts renamed to proxy.ts in Next.js 16.1?
The rename is more than cosmetic—it clarifies the architectural intent. The term "middleware" was often confused with Express-style middleware, leading developers to stuff heavy business logic into the edge layer where it hurt performance. The name Proxy explicitly defines this file as a "network boundary" meant for lightweight tasks like routing, header modification, and redirects, rather than deep application logic.
Where should I put my authentication logic now in 16.1?
Next.js 16.1 enforces a "Thin Proxy" architecture. You should split your auth logic into two parts:
- In
proxy.ts: Perform "optimistic" checks, such as verifying the presence of a session cookie to handle fast redirects (e.g., sending unauthenticated users to/login). - In Server Components/DAL: Perform authoritative validation, such as database lookups or cryptographic token verification, within your Data Access Layer or Server Components.
Does proxy.ts run on the Edge Runtime?
By default, no. Unlike the legacy middleware.ts which defaulted to the Edge Runtime, the new proxy.ts defaults to the Node.js runtime. This provides better compatibility with third-party libraries and standard Node APIs, though it implies you should avoid expensive computations in the proxy to maintain low latency.
How do I fix the "cookies() must be awaited" error in Next.js 16.1?
Next.js 16.1 requires asynchronous access to dynamic request APIs to align with React 19. You must update your code to await these calls.
- Change:
const cookieStore = cookies() - To:
const cookieStore = await cookies()This applies toheaders(),params, andsearchParamsas well.
Can I still use middleware.ts?
Technically, yes, but it is deprecated. While middleware.ts is still supported for specific Edge runtime use cases in the short term, you should migrate to proxy.ts to ensure future compatibility and access to the latest framework features in Next.js 16.1.
