Matchers and Rewrites

Matchers, Execution Order, and Config Rewrites

Without a config export the proxy runs on every request — every stylesheet, every optimized image, every file in public/. An auth redirect written without a matcher will block the CSS that styles the login page. The matcher narrows it:

Matching everything except assets (proxy.ts)TypeScript
export const config = {
  matcher: [
    // everything except API routes, Next internals and metadata files
    '/((?!api|_next/static|_next/image|favicon.ico|robots.txt).*)',
    { source: '/api/:path*', missing: [{ type: 'cookie', key: 'session' }] },
  ],
}

Strings use path-to-regexp syntax: :path matches one segment, :path* zero or more, :path+ one or more, and a pattern is anchored at the start, so /about also matches /about/team. Object entries add has and missing arrays testing a header, cookie or query parameter — the entry above runs the proxy on API calls only when no session cookie is present. Matcher values must be literal constants; one built from a variable is silently ignored.

Two exceptions are worth memorizing. _next/data routes invoke the proxy even when a negative pattern excludes them, so protecting a page cannot leave its data route open. And Server Actions are POSTs to the page that uses them, so a matcher skipping a path skips every action on it — hence the rule of Authorization Checks, that authorization is checked inside the action.

The proxy is also not first in line. next.config.ts gets two chances ahead of it and three more behind:

The order a request travels through, first to last
Step Stage
1 headers() from next.config.ts
2 redirects() from next.config.ts
3 proxy.ts
4 beforeFiles rewrites
5 Filesystem routes: public/, app/, pages/
6 afterFiles rewrites
7 Dynamic routes
8 fallback rewrites

That ordering is easy to test. The proxy in proxy.ts answers /legacy/catalog with a 308. Add a redirects() entry to next.config.ts sending /legacy/:path* to /api/products with permanent: false, and the answer becomes HTTP/1.1 307 Temporary Redirect — not the proxy's 308 — while no [proxy] GET /legacy/catalog line appears in the log. Step 2 answered before step 3 ran.

An afterFiles rewrite proves the other half. Sending /feed to /api/products returns the data yet also logs nothing, because the proxy saw the original /feed, and that rewrite happens at step 6. So a rewrite destination never re-enters the proxy: match on the URL the client asked for.

Prefer the config file where it can do the job: redirects() and headers() are static rules applied without running any of your code, so they are faster and cannot break with a logic error. Reserve proxy.ts for decisions that need the request itself — a cookie, a geography header, an A/B bucket. And test the matcher: next/experimental/testing/server exports the experimental unstable_doesProxyMatch, which asserts whether one would run for a given URL without starting a server.