Some decisions must be made before a request reaches any page or handler: redirect an old URL, choose a locale, block a bot, or attach a header for downstream code. Next.js runs a single proxy.ts file for this purpose; it was called middleware.ts until Next.js 16. After this lesson you will be able to write a proxy, scope it with a matcher, redirect and rewrite requests, and know where its limits are.
Place proxy.ts at the project root, or inside src/ if you use that layout, and export a function named proxy:
// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
return NextResponse.next(); // continue to the matched route
}The function runs on the Node.js runtime for every matched request and must return a response: NextResponse.next() to continue, or a redirect, rewrite or custom response to short-circuit. Only one proxy file is allowed per project; branch on request.nextUrl.pathname inside it.
When upgrading an older project, npx @next/codemod@latest middleware-to-proxy . renames the file and the export for you.
Without a matcher the proxy runs on every request, including static assets. Restrict it:
export const config = {
matcher: [
"/dashboard/:path*", // a section and everything beneath it
"/((?!api|_next/static|_next/image|favicon.ico).*)", // everything except these
],
};| Pattern | Matches |
|---|---|
| /about | Exactly /about |
| /blog/:slug | One segment: /blog/hello |
| /docs/:path* | Zero or more segments under /docs |
| Regex in parentheses | Anything the expression allows, as in the negative lookahead above |
Matcher values must be static strings so Next.js can analyse them at build time.
A redirect changes the URL in the browser and returns 307 or 308. A rewrite serves a different route while the address bar stays the same:
export function proxy(request: NextRequest) {
const url = request.nextUrl;
// Permanent redirect for a moved page
if (url.pathname === "/old-pricing") {
return NextResponse.redirect(new URL("/pricing", request.url), 308);
}
// Rewrite: show /beta/home to flagged users without changing the URL
if (url.pathname === "/" && request.cookies.get("beta")?.value === "1") {
return NextResponse.rewrite(new URL("/beta/home", request.url));
}
return NextResponse.next();
}Rewrites power A/B tests, locale prefixes and multi-tenant routing (mapping acme.example.com to /tenants/acme). Redirects that never change belong in the redirects() option of next.config.ts instead.
The proxy can attach request headers that Server Components and handlers later read, and set response headers or cookies:
export function proxy(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-request-id", crypto.randomUUID());
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("x-frame-options", "DENY");
response.cookies.set("visited", "1", { path: "/" });
return response;
}Downstream code reads the id with (await headers()).get("x-request-id"), a simple way to correlate logs.
What is the name of the file that intercepts requests in Next.js 16?
proxy.ts (formerly middleware.ts) runs before routing for every request matched by config.matcher.NextResponse.next(), a redirect, a rewrite or a custom response.NextResponse.next({ request: { headers } }).Next lesson: Styling: CSS Modules, Tailwind and Global CSS ā the built-in ways to style a Next.js application.