Proxy (formerly Middleware)
Sometimes you need to act on a request before any page renders: send visitors from an old URL to a new one, show half of your visitors a new design, add a security header to every response, or bounce users without a session cookie away from /dashboard. Next.js gives you one file for this, called Proxy.
In this lesson you will write a proxy.ts file, limit it to certain paths with a matcher, redirect, rewrite and set headers and cookies, learn where Proxy runs in the request order, and see why it was renamed from Middleware in Next.js 16.
The proxy file
Create proxy.ts (or proxy.js) in the project root, next to the app folder (or inside src if you use one). Export a function named proxy, or a default export. Only one proxy file is allowed per project, but it can import helpers from other files.
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname === "/old-fees") {
return NextResponse.redirect(new URL("/fees", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/old-fees", "/dashboard/:path*"],
};The function receives a NextRequest and returns one of these:
| Return value | Effect |
|---|---|
NextResponse.next() | Continue to the page as normal (optionally with changed headers) |
NextResponse.redirect(url) | Tell the browser to go to another URL; the address bar changes |
NextResponse.rewrite(url) | Show another route's content while the address bar keeps the original URL |
Response.json(...) or another Response | Answer directly without rendering a page |
| nothing | Same as continuing |
A redirect is visible: the user sees the new URL. A rewrite is invisible: the user stays on /offers while Next.js renders, say, /offers-v2. Rewrites are handy for A/B tests.
Matchers: choose where Proxy runs
Without a matcher, Proxy runs on every request, including JavaScript files in _next/static, optimised images in _next/image and files from public. A mistake in an auth redirect can then block your CSS and images. The matcher limits it:
- A single path:
matcher: "/about" - Several paths:
matcher: ["/about", "/contact"] - Path parameters:
"/dashboard/:path*"matches/dashboardand anything below it (*means zero or more parts,+one or more,?zero or one) - A regular expression in brackets, often a "negative lookahead" that matches everything except some paths:
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"],
};Matcher values must be constants, because Next.js reads them at build time. The runnable example tests the same negative lookahead as a JavaScript regular expression, and then runs a small decision function shaped like a proxy:
import { createRoot } from "react-dom/client";
// The same idea as the matcher above, written as a plain JS RegExp
const matcher = /^\/(?!api|_next\/static|_next\/image|favicon\.ico|sitemap\.xml|robots\.txt).*$/;
const paths = ["/", "/dashboard/fees", "/api/courses", "/_next/static/chunk.js", "/favicon.ico", "/login"];
for (const p of paths) {
console.log(p.padEnd(24), matcher.test(p) ? "proxy runs" : "skipped");
}
// A proxy-like decision: protect /dashboard, redirect old URLs
function decide(pathname, cookies) {
if (pathname === "/old-fees") return "redirect -> /fees";
if (pathname.startsWith("/dashboard") && !cookies.session) return "redirect -> /login?next=" + pathname;
return "next()";
}
console.log(decide("/dashboard/fees", {}));
console.log(decide("/dashboard/fees", { session: "abc123" }));
console.log(decide("/old-fees", {}));
createRoot(document.getElementById("root")).render(<p>Open the console output below the editor.</p>);/ proxy runs /dashboard/fees proxy runs /api/courses skipped /_next/static/chunk.js skipped /favicon.ico skipped /login proxy runs redirect -> /login?next=/dashboard/fees next() redirect -> /fees
Headers and cookies
Proxy can read and change headers and cookies. To pass a header to your app (so a Server Component can read it with headers()), set it on the request inside NextResponse.next({ request: { headers } }). To send a header to the browser, set it on the response:
// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-city", request.cookies.get("city")?.value ?? "Delhi");
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("X-Frame-Options", "DENY");
if (!request.cookies.has("visitor")) {
response.cookies.set("visitor", crypto.randomUUID(), { httpOnly: true, sameSite: "lax", path: "/" });
}
return response;
}A common slip is writing NextResponse.next({ headers: requestHeaders }), which sends your request headers to the client instead of to your app.
Where Proxy fits in the request order
For each request, Next.js checks things in this order:
headersfromnext.config.tsredirectsfromnext.config.ts- Proxy
beforeFilesrewrites fromnext.config.ts- Files and routes:
public,_next/static,appandpages afterFilesrewrites, then dynamic routes, thenfallbackrewrites
So for a fixed list of redirects that never change, prefer the redirects option in next.config.ts: it is simpler and runs before Proxy. Use Proxy when the decision needs the request: a cookie, a header, the visitor's country or a random A/B split.
What Proxy is not for
Proxy runs on every matched request, including prefetches, so keep it fast. It is not the place for slow data fetching, and it should not be your only security check:
- Do quick, optimistic checks in Proxy, such as "is there a session cookie?", and redirect early.
- Do the real authorisation next to the data: in your data access functions, Server Actions and Route Handlers. Server Actions are
POSTrequests to the page where they are used, so a matcher that skips a path also skips its actions. fetchcaching options have no effect inside Proxy.
Proxy runs on the Node.js runtime, and the runtime cannot be changed. You can also use event.waitUntil(promise) from the second argument to finish background work, such as logging, after the response is sent.
Common mistakes
- No matcher on an auth redirect. Every image and script request gets redirected to
/logintoo. Exclude static paths. - Doing database work in Proxy. It slows every request. Read the cookie only; check the database later.
- Relying on Proxy alone for security. Always check permissions again in actions and data functions.
- Two proxy files. Only one
proxy.tsis allowed; split logic into imported modules instead.
Key points
proxy.tsin the project root runs code before routes render; exportproxy(or default) and an optionalconfig.matcher.- Return
NextResponse.next(),redirect(),rewrite()or aResponse. - Use matchers to skip static files, images and APIs.
- Set request headers with
NextResponse.next({ request: { headers } }); set response headers and cookies on the response. - Middleware was renamed Proxy in Next.js 16; keep Proxy fast and do real authorisation near your data.
Exercise
Write a proxy.ts that (1) redirects /admission to /admissions permanently in the URL bar, (2) rewrites /offers to /offers-b for visitors whose ab cookie equals "b", and (3) sends visitors without a session cookie from any /student/... page to /login?next=PATH. Add a matcher so it only runs where needed.
Show solution
// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === "/admission") {
return NextResponse.redirect(new URL("/admissions", request.url), 308);
}
if (pathname === "/offers" && request.cookies.get("ab")?.value === "b") {
return NextResponse.rewrite(new URL("/offers-b", request.url));
}
if (pathname.startsWith("/student") && !request.cookies.has("session")) {
const login = new URL("/login", request.url);
login.searchParams.set("next", pathname);
return NextResponse.redirect(login);
}
return NextResponse.next();
}
export const config = {
matcher: ["/admission", "/offers", "/student/:path*"],
};Status 308 makes the redirect permanent. In a real app, the fixed /admission redirect could live in next.config.ts instead, leaving Proxy for the cookie-based rules.
Quick Quiz
1. What is the Next.js 16 name and file for what used to be Middleware?
Next.js 16 renamed Middleware to Proxy; the file is proxy.ts and the function proxy.
2. What is the difference between a redirect and a rewrite?
Redirects send the browser elsewhere; rewrites change what is rendered behind the same URL.
3. Why add a matcher that excludes _next/static and _next/image?
Without a matcher, Proxy runs on every request, including static assets.
4. Where should the real permission check for deleting a course happen?
Proxy is for fast, optimistic checks; secure checks belong next to the data.
Learn Next.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
