Orchestrating Requests in Proxy
Proxy (formerly Middleware) as a single checkpoint that runs before a route renders — auth redirects, region personalization, and security headers decided once instead of scattered across every page.
View the live demo →One checkpoint, before routing and rendering decide anything
Proxy is code that runs before a request's routing and rendering decisions are finalized — early enough to inspect a request, redirect it, rewrite it, or attach headers, and late enough to already know the pathname, cookies, and headers the browser sent. It defaults to the Node.js runtime, so it has the same APIs a Route Handler does, just running one step earlier.
This file convention was named middleware.ts through Next.js 15. Next.js 16 renamed it to proxy.ts — same request/response API, exported as proxy instead of middleware — because "middleware" kept getting confused with the Express.js kind, which runs handlers in a chain rather than making a single early routing decision.
A project has exactly one Proxy file. Everything it should and shouldn't touch is decided by one thing: its matcher.
Without it, the same check gets copy-pasted into every route
A session check that lives inside every protected page is a check that has to be remembered every time a new protected page gets added — and forgotten exactly once for a route to quietly ship without it. Proxy turns "does every route that needs auth check for it" into "does the one file at the project root check for it," run once per matched request instead of once per route.
request.cookies.get("auth-token")
→ no valid session, pathname under /dashboard-demo
→ redirect to /login-demo?returnTo=/dashboard-demoThe same request also carries a country code and a browser that will render whatever headers the response sends back — two more decisions ("which region's content" and "which security headers apply") that don't need the destination route's own logic to make.
Keep it to the decisions that don't need a database
Auth/token validation, geolocation-based personalization, feature flags, redirects, and security headers are the recurring list of things worth centralizing here. Proxy is a poor fit for anything that needs a real query — Postgres round trips or heavy business logic belong in the Route Handler or Server Component the request eventually reaches, not in the checkpoint every request passes through first.
This demo's check is deliberately shallow for that reason: it verifies a signed JWT's claims, which is CPU work with no I/O, and nothing heavier. A real app might additionally confirm the session hasn't been revoked — that's the kind of check that belongs downstream instead, once, in whatever already talks to the database.
Proxy makes the request-level decision. It doesn't do the application's work — it hands the request to whatever does.
Proxy redirects away. It doesn't protect what runs after.
A matcher change, or a route that moves, can silently drop Proxy's coverage without anything erroring — the route just starts rendering for requests Proxy used to redirect. The dashboard demo below re-verifies its own session cookie on render rather than trusting that Proxy already handled it, which is what the demo's two auth checks — one in Proxy, one in the page — are actually showing.
The whole lifecycle, in one picture
Everything above described one piece at a time. Here's how they fit into a single request's trip through the demo — the exception path (dashed, looping back through login-demo) versus the path a request with a session actually takes.
login-demo before returning to the same checkpoint, rather than dead-ending.The dashboard demo's structure
Visit the live demo without a session and Proxy redirects you to login-demo before anything renders. Sign in as one of two demo personas there and it hands back a signed JWT in an auth-token cookie, which is what gets you back into dashboard-demo on the next request.
src/proxy.ts ← the one Proxy file, matcher-scoped
to /patterns/request-orchestration/*
app/patterns/request-orchestration/
├── data.ts ← JWT sign/verify, demo personas,
│ country → region mapping
├── dashboard-demo/
│ └── page.tsx ← protected; re-checks its own session
└── login-demo/
├── page.tsx ← persona + simulated-country picker
└── actions.ts ← signs the JWT, sets/clears cookies
components/patterns/request-orchestration/
├── PersonaPicker.tsx
├── RegionPicker.tsx
├── SecurityHeadersPanel.tsx ← re-fetches the page, reads its own
│ response headers back
└── SignOutButton.tsxThe region picker stands in for a CDN header. Production deployments get a geolocation signal for free on every request (Vercel's x-vercel-ip-country, for instance) — NextRequest.geo itself was removed in Next.js 15. This demo has no CDN in front of it, so picking a country writes a cookie that Proxy reads the same way it would read that header.
The personalization header is set on the request, not the response. NextResponse.next({ request: { headers } }) forwards the decided region to whatever renders next, so the dashboard page can read it back with headers(). Setting it on the response instead would only ever reach the browser's network tab, not any server code downstream.
The security headers panel on the live demo re-fetches the current page and reads its own response headers back out — proof that Proxy's headers landed on the actual response, not just in the source.