Lesson 54 of 81
Without a matcher, proxy.ts runs on every single request — CSS, images, the favicon, all of it. The matcher config is how you tell it where to actually bother, and its syntax is stricter than it first looks.
12 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.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on 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
The config.matcher export alongside proxy.ts's proxy function scopes which requests actually invoke it. Leave it off, and proxy runs on every request in the project — _next/static, _next/image, every file in public/, all of it — which is rarely what anyone actually wants and can measurably slow down or break asset loading if the function itself does anything nontrivial.
matcher accepts a string, an array of strings, or an array of objects with source (the path pattern), plus optional has/missing (match based on the presence or absence of a header, cookie, or query param) and locale (opt out of locale-aware matching). Whatever you pass has to be a static, literal value — Next.js analyzes it at build time, so a matcher built from a variable or computed at runtime is silently ignored.
// proxy.ts
import { type NextRequest, NextResponse } from "next/server";
export function proxy(request: NextRequest) {
// ...auth check, same as any other proxy.ts
return NextResponse.next();
}
export const config = {
matcher: [
// Everything except static assets, image optimization files, and favicon
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
// Only /api requests that are NOT an internal prefetch
{
source: "/api/:path*",
missing: [{ type: "header", key: "next-router-prefetch" }],
},
],
};
The first pattern is a negative lookahead — (?!...) — reading as "match any path that does not start with one of these." It's the standard shape for "run on everything except assets," and it's regex, not a glob, so the escaping (\\. for a literal dot) matters. The second entry is the object form: it still matches by source, but adds missing, which skips the function entirely for a request carrying Next's own internal prefetch header — useful when a proxy does real work (a database check, a slow auth lookup) that a background prefetch shouldn't trigger.
proxy.ts written and tested against one specific route, shipped with no matcher, now running (and paying whatever cost it has) on every CSS file, every font, every image on the site.matcher: buildMatcher() or matcher: process.env.MATCHER_PATTERN gets ignored — Next.js needs to read the literal array or string at build time without executing your code, so anything computed is treated as if it weren't there._next/data routes run proxy even when a negative-lookahead pattern appears to exclude them. This is deliberate: it exists specifically to prevent the scenario where a page is protected but its corresponding client-side data route is accidentally left open because someone's matcher pattern excluded anything starting with _next.matcher that excludes a path also exempts Server Function calls made from that path — they're POSTs to the same route, not a separate kind of request. Scoping a matcher to, say, only /dashboard/:path* doesn't just skip rendering proxy for pages outside that prefix — it skips it for any Server Action invoked from those pages too, which is exactly the gap that makes "verify inside the Server Function itself" the real fix rather than relying on the matcher as the only gate.has/missing aren't just path-shape filters — they can check headers, cookies, and query params independently of the URL. A matcher scoped correctly by path can still run (or not run) differently depending on whether a specific cookie is present, which is easy to miss if you're only reading the source string and ignoring the rest of the object.The dedicated lesson on proxy and request interception covers proxy.ts itself — what it can do, its execution order relative to next.config.js rewrites, and the Server Function coverage gap. This lesson is the part of that file people get wrong most mechanically: the matcher syntax specifically, and the concrete ways an overly broad or overly narrow pattern causes real bugs. The lesson on the middleware-to-proxy rename is a different angle again — that one's about the name change itself, not the config shape underneath it.