Overview

A proxy.ts file runs before a matched request reaches a route and can rewrite, redirect, set headers or cookies, or respond directly. Next.js 16.0 renamed middleware.ts to proxy.ts to make the network boundary explicit, and the Next.js docs recommend it as a last resort when no other API fits. It always runs on the Node.js runtime. Because it runs on every matched request, its cost compounds.

Place proxy.ts at the project root and export proxy

Put one proxy.ts at the project root (or inside src/ next to app/). Export a function named proxy or a default export; the Next.js docs recommend the name proxy even for default exports.

// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function proxy(req: NextRequest) {
  return NextResponse.next();
}

Migrate with npx @next/codemod@canary middleware-to-proxy .; it renames the file and function, and skipMiddlewareUrlNormalize becomes skipProxyUrlNormalize. middleware.ts still works but is deprecated, and it is the only way to keep the Edge runtime. The runtime config option throws inside proxy.ts. Do not share module state or globals with the app, because a proxy may run apart from it, for example on a CDN.

Constrain the matcher

Without a matcher the proxy runs on every request, including _next/static, _next/image, and public/ files, and auth logic can block CSS and images. Matcher values must be constants. A request for _next/data still invokes the proxy even when a pattern excludes it.

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"],
};

For specific areas, list them: matcher: ["/dashboard/:path*", "/admin/:path*"].

Pick redirect, rewrite, or next deliberately

  • NextResponse.redirect(url): the browser navigates and the URL changes. Use for canonical URLs and moved pages.
  • NextResponse.rewrite(url): serves another route under the original URL. Use for A/B routing and locale serving.
  • NextResponse.next(): continues to the route, optionally with changed headers or cookies.

NextRequest has no geo or ip property since Next.js 15; read the host’s headers, for example x-vercel-ip-country on Vercel.

Forward data through request headers

Copy the request headers, add yours, and pass them in request. Passing { headers } at the top level sends them to the client instead.

const headers = new Headers(req.headers);
headers.set("x-user-id", userId);
return NextResponse.next({ request: { headers } });

Read them downstream with headers() from next/headers. Set auth cookies with httpOnly, secure, and an explicit sameSite; see auth-sessions.

Check auth again inside every server action and data call

Server Actions are POST requests to the route that uses them, so a matcher that skips a path also skips actions on it, and moving an action to another route can silently drop proxy coverage. Use the proxy for coarse gating such as redirecting anonymous users, and verify authentication and authorization inside each action, handler, and data-access function. See nextjs-server-actions.

Keep the proxy fast

  • Verify tokens locally or cache the result; do not call the auth provider on every request.
  • Avoid external API calls and large body parsing; move that work into the route.
  • Use event.waitUntil(promise) for background work such as logging, so the response is not delayed.
  • Cache invalidation is unavailable here: revalidateTag cannot be called from the proxy.