Lesson 19 of 81
Before a single component renders, something can already have decided this request doesn't get to — redirecting an unauthenticated visitor, rewriting a path, attaching a header. That gatekeeper is proxy.ts, and this very app's own auth gating runs through exactly this file.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A project upgraded to Next.js 16 still has a file named `middleware.ts` exporting `export function middleware(request) { ... }`. What's the current, non-deprecated way to write this?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
proxy.ts is a file at the project root that runs on the server for matching requests before any route renders. It can inspect the incoming request, then redirect it elsewhere, rewrite it to a different path, modify request or response headers, or respond directly — all ahead of any page or Route Handler ever executing.
This file used to be called middleware.ts, and the function it exported used to be named middleware. As of Next.js 16.0.0, that convention is deprecated and renamed: the file is proxy.ts, and the exported function is named proxy. The rename isn't cosmetic — "middleware" kept getting confused with Express.js middleware (a chain of handlers inside your app's request lifecycle), when what this file actually does is act like a proxy sitting in front of your app, intercepting a request before it ever reaches your rendering code. Proxy also now defaults to the Node.js runtime, not the Edge runtime it used to default toward.
/learn behind auth// proxy.ts — project root
import { type NextRequest } from "next/server";
import { updateSession } from "@/lib/supabase/middleware";
export async function proxy(request: NextRequest) {
return updateSession(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
A request for /dashboard/settings arrives. proxy.ts runs before any route renders.
This is this very app's real proxy.ts. updateSession reads the Supabase auth cookie off the incoming request, and if the request is headed for a protected path (/learn, /dashboard, etc.) with no valid session, it returns a redirect to /login before app/learn/[track]/[lesson]/page.tsx ever gets a chance to render. The matcher excludes static assets and image optimization files — without that, every CSS file and every image request would also pay the cost of running this check.
proxy (or be the default export), not middleware. Code copied from an older tutorial or an existing project on an older Next.js version will still say middleware.ts / export function middleware — that convention is deprecated in v16, and a codemod (npx @next/codemod@canary middleware-to-proxy .) exists specifically to migrate it.matcher, Proxy runs on every single request, including _next/static, _next/image, and everything in public/. Forgetting to scope or negatively-match those paths is a common way to accidentally slow down or break asset loading.next.config.js's beforeFiles rewrites, but after its headers and redirects. The real order is: config headers → config redirects → Proxy → config beforeFiles rewrites → filesystem routes → config afterFiles rewrites → dynamic routes → config fallback rewrites. Proxy isn't first in line, and it isn't the only place routing decisions happen.matcher excluding a path also excludes Server Function calls made from that path — this is the security-relevant gotcha above, and it's easy to miss because a Server Action doesn't look like "a route" the way a page does.runtime route-segment-config option isn't available inside proxy.ts at all — setting it there throws an error, since Proxy's runtime is fixed at the file-convention level, not configurable per-file."Where does your app actually enforce that /dashboard requires a signed-in user?" is a real, common question, and "in the page component" is a wrong answer in the App Router — this app enforces it in proxy.ts, and a candidate who can explain why Proxy is the right layer for that (it runs before rendering, for every matching request) versus why it's not sufficient alone (Server Functions can bypass a scoped matcher) is showing real, load-bearing understanding rather than a memorized file name.