Proxy and Authentication · Lesson 20 of 24
Next.js Proxy (Middleware): Redirects and Rewrites
A proxy.ts file at the root of your project runs before a request reaches any page or route handler. It can redirect, rewrite the URL to another page, add headers or cookies, or answer the request directly. In Next.js 16 this feature was renamed from Middleware to Proxy – it works the same way, and you'll still see middleware.ts in older projects.
In this lesson
- Writing
proxy.ts: redirects and rewrites - Reading cookies and setting headers
- Choosing routes with
matcher - What Proxy should and shouldn't do
Redirects and rewrites
// proxy.ts
// lives at the project root, next to app/
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Redirect: old URLs → new URLs (the browser's address bar changes)
if (pathname.startsWith("/blog/")) {
return NextResponse.redirect(new URL(pathname.replace("/blog/", "/articles/"), request.url), 308);
}
// 2. Rewrite: show a different page without changing the URL
if (pathname === "/offers" && request.cookies.get("plan")?.value !== "pro") {
return NextResponse.rewrite(new URL("/offers/upgrade", request.url));
}
return NextResponse.next(); // continue as normal
}
export const config = {
matcher: ["/blog/:path*", "/offers"], // only run for these paths
};
// app/articles/[slug]/page.tsx
export function generateStaticParams() {
return [{ slug: "nextjs-16" }];
}
export default async function Article({ params }: PageProps<"/articles/[slug]">) {
const { slug } = await params;
return <h1>Article: {slug}</h1>;
}
// app/offers/page.tsx
export default function Offers() {
return <h1>Pro member offers: 50% off mentoring</h1>;
}
// app/offers/upgrade/page.tsx
export default function Upgrade() {
return <h1>Upgrade to Pro to unlock offers</h1>;
}
GET /blog/nextjs-16 → 308 (redirect to /articles/nextjs-16) /articles/nextjs-16 URL: /articles/nextjs-16 Article: nextjs-16 › open /offers URL: /offers Upgrade to Pro to unlock offers
| Redirect | Rewrite | |
|---|---|---|
| URL in the address bar | changes | stays the same |
| Extra round trip | yes (a 3xx response) | no |
| Typical uses | moved pages, login redirects, forcing a locale prefix | A/B tests, paywalls, multi-tenant sites, feature flags |
For a fixed list of moved URLs, the redirects() option in next.config.ts is simpler. Use Proxy when the decision depends on the request – cookies, headers, the user's country or device.
Headers and cookies
// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("x-frame-options", "DENY"); // a response header
if (!request.cookies.has("visitor")) {
response.cookies.set("visitor", crypto.randomUUID(), { httpOnly: true, sameSite: "lax", maxAge: 60 * 60 * 24 * 365 });
}
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], // everything except static assets
};
// app/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
async function Visitor() {
const id = (await cookies()).get("visitor")?.value;
return <p>Visitor cookie: {id ? `present (${id.length} characters)` : "missing"}</p>;
}
export default function Home() {
return (
<Suspense fallback={<p>…</p>}>
<Visitor />
</Suspense>
);
}
Visitor cookie: present (36 characters) › reload the page Visitor cookie: present (36 characters)
Notice the cookie is already "present" on the very first visit. Cookies that Proxy sets on the response are also made visible to Server Components handling the same request, and the browser stores them for every later one.
The matcher
Without a config.matcher, Proxy runs on every request, including JavaScript chunks and images. Matchers accept path patterns (/dashboard/:path*), arrays, and regular expressions like the one above that excludes static files. Matcher values must be constants – they're read at build time.
What Proxy is (and isn't) for
- Good: redirects and rewrites, locale detection, A/B test bucketing, security headers, bot filtering, quick "is there a session cookie?" checks.
- Avoid: slow work such as database queries – Proxy runs before every matched request, including prefetches. Keep it fast.
- Not your security boundary: Server Actions are POST requests to the page that uses them, and a matcher change can quietly stop covering them. Always check authentication and permissions again where the data is read or changed (next lesson).
Proxy uses the Node.js runtime by default, so most npm packages work in it.
Practice
Try it yourself
Redirect
/docs(and anything under it) to/learning/nextjswith a permanent 308 status.Show solution
// proxy.ts import { NextResponse, type NextRequest } from "next/server"; export function proxy(request: NextRequest) { return NextResponse.redirect(new URL("/learning/nextjs", request.url), 308); } export const config = { matcher: ["/docs", "/docs/:path*"] };// app/learning/nextjs/page.tsx export default function Course() { return <h1>Next.js course</h1>; }GET /docs/routing → 308 (redirect to /learning/nextjs) /learning/nextjs
Quick check
What was Proxy called before Next.js 16?
Middleware (middleware.ts).
Does a rewrite change the URL in the address bar?
No – a redirect does; a rewrite serves another route at the same URL.
Is a session check in Proxy enough to protect your data?
No – verify the session again in the data layer, Server Actions and Route Handlers.
Summary
- proxy.ts runs before matched requests: redirect, rewrite, set headers and cookies, or respond.
- Limit it with a constant matcher, and keep it fast.
- Use it for routing decisions and optimistic checks – never as the only security check.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.