In a Next.js proxy file, the matcher config decides which request paths actually run the proxy function. It is the difference between intercepting one section of a site and intercepting every asset the browser downloads.
Without a matcher, Proxy runs on every request, including files under the static and image optimization paths and anything in the public folder. Redirect logic written for pages then applies to stylesheets and images too, which is how a proxy file breaks a site that looked fine in review.
The narrowest useful matcher is a single path pattern next to the function.
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/login', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}A request to /dashboard or /dashboard/billing/invoices runs the function and receives the redirect. A request to /pricing or to a stylesheet never enters it, so those responses are untouched.
How path patterns are read
Matcher patterns are not plain string comparisons. They use the path-to-regexp syntax, which the config redirects and rewrites options share, so the rules you learn here transfer to those.
Three rules cover most cases. Patterns must start with a forward slash, they are anchored to the start of the path, and a segment beginning with a colon is a named parameter.
| Pattern | Matches | Does not match |
|---|---|---|
| /about | /about and /about/team | /blog/about |
| /about/:slug | /about/team | /about/team/history |
| /about/:slug* | /about, /about/a, /about/a/b | /docs/about |
The modifier after a named parameter controls repetition. A star means zero or more segments, a plus means one or more, and a question mark means zero or one.
To cover several unrelated areas, pass an array instead of a string. Each entry is evaluated independently, and a request that matches any of them runs the function.
// proxy.ts
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/api/private/:path*'],
}This runs Proxy for the three protected areas and skips everything else. An array is the clearest option whenever you can name the paths, because it needs no regex reading to review.
Excluding assets with a negative pattern
Some rules genuinely are site-wide. Adding a security header to every HTML response is the usual example, and listing every page path would be unmaintainable.
For that case the matcher accepts a regular expression, so a negative lookahead expresses "everything except these" directly.
// proxy.ts
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
}Requests for pages run the function, while API routes, build assets, optimized images, and the metadata files are skipped. Adjust the exclusion list to match what your project actually serves, because a path you forget is a path that pays the cost of the function.
Next.js still invokes Proxy for requests under the internal data path even when a negative pattern excludes it. The reason is safety: without it, protecting a page while leaving its data route open would be an easy mistake to make.
Both forms can be mixed. An array can hold plain path patterns and a regular expression together, which is useful while migrating a broad rule into narrower ones.
Matching on headers, cookies, and queries
A matcher entry can also be an object instead of a string. That form takes a source pattern plus conditions, so the function runs only when the request carries, or is missing, a particular header, cookie, or query value.
// proxy.ts
export const config = {
matcher: [
{
source: '/api/:path*',
has: [{ type: 'header', key: 'authorization' }],
missing: [{ type: 'cookie', key: 'session' }],
},
],
}Every source, every has entry, and none of the missing entries must match for the function to run. Leaving off a value, as with the authorization header above, means any value satisfies the condition.
Skipping prefetch requests is the most common real use. Router prefetches arrive with a prefetch header, and a matcher that excludes them keeps expensive interception off requests the user has not committed to yet.
An object entry also accepts a locale flag. Setting it to false compares the path without any locale prefix, which is worth knowing before you build locale routing on top of Proxy.
Matchers must be static
Next.js reads matcher values at build time so it can decide, per route, whether the proxy function needs to be invoked at all. A value that only exists at runtime cannot be read then.
Generating the array from a variable looks reasonable and is not allowed. The build refuses it rather than shipping a proxy file whose scope cannot be determined.
// proxy.ts
const protectedAreas = ['/dashboard', '/settings']
export const config = {
matcher: protectedAreas.map((area) => `${area}/:path*`),
}Both next dev and next build refuse it, naming the exact expression they could not read. This is a helpful failure, because the alternative would be a matcher that quietly covers nothing at all in production.
Next.js can't recognize the exported `config` field in route "/proxy":
Unsupported node type "CallExpression" at "config.matcher".
Read More - https://nextjs.org/docs/messages/invalid-page-configWrite the literal strings in the config export instead. If the list is long, keep it readable with an array of plain strings rather than generating it, and accept the small amount of repetition.
The same constraint explains why request-dependent decisions belong inside the function body. Matching narrows the set of requests, and the function then reads the request to decide what to do.
Verifying what a matcher covers
The browser is the fastest check. Open the network panel, request a path you expect to be covered, and look for the header, redirect, or rewrite your function produces. Then request an excluded path and confirm the response is unchanged.
For coverage you want to keep, Next.js ships an experimental testing helper that answers the matching question directly without starting a server.
// proxy.test.ts
import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'
import { config } from './proxy'
test('skips the marketing homepage', () => {
expect(unstable_doesMiddlewareMatch({ config, url: '/' })).toBe(false)
})The helper reports whether the matcher would run the function for a URL, which is exactly the assertion a matcher regression needs. The documentation now calls it unstable_doesProxyMatch, but the published 16.3 package still exports it under the older middleware name, so import it exactly as written above and check the name again after an upgrade.
It is experimental, so treat it as a fast unit check rather than a substitute for debugging a proxy that never runs.
Common matcher mistakes
Most matcher bugs fall into a few shapes, and each has a visible symptom worth recognizing.
- Omitting the matcher entirely, which shows up as slow or broken static assets.
- Forgetting the leading slash, which stops the pattern from matching anything.
- Expecting a bare path to cover nested segments, when it needs a parameter and a star modifier.
- Building the value from a variable, which stops the build instead of producing a working matcher.
The habit that prevents all four is starting narrow. Match the one path the feature needs, confirm the behavior in the browser, then widen the pattern only when a second path genuinely needs the same treatment.
Rune AI
Key Insights
- Without a matcher, proxy runs on every request including static assets and optimized images.
- A matcher accepts a single path string, an array of paths, or objects with source, has, and missing.
- Path patterns are anchored to the start and support named parameters with the star, plus, and question mark modifiers.
- A negative lookahead pattern is the standard way to write a site-wide rule that skips assets.
- Matcher values must be static constants because Next.js reads them at build time.
Frequently Asked Questions
What happens if I do not export a matcher?
Can a matcher be built from a variable?
Why does proxy still run for _next/data requests?
Does the matcher control the response, or only whether proxy runs?
Conclusion
The matcher is the scope control for a proxy file. Start with the narrowest list of paths that the feature actually needs, use a negative lookahead only when a site-wide rule is genuinely required, and remember that matchers must be static literals that Next.js can read at build time.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.