skip to content

A Next.js middleware file exports `config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] }`. Explain what that pattern matches and why those particular prefixes are excluded.

level: middleimportance: must knowfreq 60%

answer

  1. deny-list inside a catch-all
  2. zero-width assertion, consumes nothing
  3. middleware precedes filesystem routes
  4. assets outnumber documents per page view
  5. forgotten prefix means included

basics

~20 s

That pattern matches every request path except ones beginning with api, _next/static, _next/image, or favicon.ico. Middleware runs ahead of filesystem routes, so without the exclusions every build asset and optimized image would pay a middleware invocation.

solid answer

~50 s

The entry is a single pattern rooted at `/`, wrapped around a negative lookahead: `(?!api|_next/static|_next/image|favicon.ico)` asserts that what follows the leading slash does *not* begin with any of those prefixes, and `.*` then consumes the rest. So it is a deny-list expressed as one catch-all — match everything, minus these. The exclusions are a cost decision first. Middleware sits ahead of filesystem routes in Next's request pipeline, so a request for a hashed bundle under `/_next/static` or for the image optimization endpoint under `/_next/image` reaches middleware unless you exclude it. Those requests are the bulk of a page load and there is nothing useful to do with them, so every invocation is pure latency and, on hosts that bill per invocation, pure cost. It is also a correctness decision: logic written for page requests — locale detection, redirects, header rewriting — usually makes no sense applied to a JavaScript chunk or an API call from a machine client.

code

typescript · 14 lines
typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  return NextResponse.next()
}

export const config = {
  matcher: [
    // everything except API routes, build output, the image
    // optimizer, the favicon, and common static file types
    '/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

go deeper

for a junior

Be able to read the pattern aloud: everything under the root except paths starting with those prefixes. Know that _next/static holds build output and _next/image is the image optimizer.

for a middle

Explain the negative lookahead precisely and place middleware ahead of filesystem routes in the request pipeline — that ordering is the whole reason asset requests would otherwise reach your function.

for a senior

Quantify it: assets and RSC payload requests dominate the request count, so an unscoped matcher multiplies invocations, latency and per-invocation cost by something unrelated to page views. Say how you would verify the real matched population.

for a principal

Frame deny-list versus allow-list as a policy choice about exhaustiveness against blast radius, and put a guardrail around it — a test asserting which representative paths match, so an edit to one regex cannot quietly change what enters middleware.

## Reading the pattern ``` /((?!api|_next/static|_next/image|favicon.ico).*) ``` Break it apart: - The leading `/` is mandatory — every matcher entry describes a URL path and must start with a slash. - `(...)` is a group covering the rest of the path. - `(?!...)` is a **negative lookahead**: a zero-width assertion that says "at this position, the text that follows must not match this alternation". It consumes nothing itself. - `api|_next/static|_next/image|favicon.ico` is the alternation of excluded prefixes. - `.*` then matches the remainder of the path. So the whole entry reads: match any path, unless immediately after the leading slash it begins with `api`, `_next/static`, `_next/image` or `favicon.ico`. It is a deny-list expressed inside a catch-all, which is the shape Next's own documentation uses as the starting point. A common extension is to exclude static file extensions as well, for files served straight out of `public/`: ``` /((?!api|_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*) ``` ## Why the exclusions exist: the pipeline order The reason this is a real question and not regex trivia is where middleware sits. Next processes an incoming request roughly in this order: headers and redirects declared in the Next config, then **middleware**, then rewrites, then filesystem routes — `public/`, `_next/static`, and your `app` routes — then dynamic routes and fallbacks. Middleware is at step three; the filesystem is later. That ordering is what makes middleware useful (you can rewrite or redirect before anything has resolved) and it is also what makes an unscoped matcher expensive: the framework has not yet decided that `/_next/static/chunks/main-8f3a1c.js` is a build artifact when it consults your matcher. It is just a path. If your matcher accepts it, your function runs. ## The cost argument Count the requests a single page view generates: one document, several JavaScript chunks, a stylesheet, a few optimized images, a favicon, and — once the user starts navigating — an RSC payload request per navigation and often one per prefetched link. The document is a small minority of that traffic. An unscoped middleware turns each of the rest into an extra hop through your code. That hop costs three things. Latency, because it is on the critical path of every matched request and asset requests are otherwise served without touching application logic. Compute, because middleware is bundled, loaded and executed. And on hosts that meter middleware separately from rendering, money — the invocation count is driven by asset requests, not by page views, so the bill scales with something you were not thinking about. ## The correctness argument Cost is the headline, but excluding these prefixes also prevents outright bugs. Middleware written for pages tends to assume a browser navigating: it may attach cookies, negotiate a locale from `Accept-Language`, rewrite the path, or redirect. Applying any of that to `/_next/static/...` produces nonsense — a redirected chunk request is a broken page, and a `Set-Cookie` on an immutable asset response is at best noise. `/_next/image` is the optimizer endpoint; rewriting its path breaks image delivery. Excluding `api` similarly keeps page-shaped logic away from machine clients that will not follow a redirect to a login page and expect a JSON error instead. ## Where the deny-list shape helps and hurts A deny-list matcher is *exhaustive by default*: any route you add later is covered without touching the config. For concerns that must never be missed, that is the right property. The cost is that anything you forget to exclude is included — a new asset directory, a well-known path such as `/robots.txt` or `/.well-known/...`, a health-check endpoint. The opposite shape, an allow-list of explicit paths, has the mirror-image tradeoff: minimal invocations, but a route added six months later silently falls outside it. Whichever shape you pick, treat the matcher as reviewable configuration. A cheap safeguard is a test that asserts a handful of representative paths — an asset path, an API path, a page path — are or are not matched, so an edit to that regex cannot quietly change the traffic population entering middleware.

  • How would you write the opposite shape — an allow-list — and when is that the better choice?
    List the paths explicitly, for example `matcher: ['/dashboard', '/account']`, optionally with a regex group to cover a subtree. It is better when middleware serves a narrow concern on a small part of the app: invocations stay minimal and a bug's blast radius is bounded. The cost is that routes added later are silently uncovered, so it suits stable, well-known path sets.
  • You excluded /_next/static but traffic still shows heavy middleware invocation. What else is likely hitting it?
    Client-side navigation and link prefetching. Both fetch the RSC payload for the target route over HTTP at the route's own pathname, so they match a page-shaped matcher just as a full document request does. Middleware invocations therefore track link hovers and navigations, not page views. Prefetches can be filtered with a `missing` condition on the `next-router-prefetch` header.
  • Does excluding a path from the matcher mean requests to it are unprotected?
    It means middleware does not run for it, so any check implemented there does not apply. That is the correct framing: a matcher is a scoping and cost mechanism, and whatever guarantees a path needs must hold in the layer that actually serves it. Excluding `api` from a matcher is a statement about where logic lives, not a claim that the path needs nothing.

saying these in an interview costs you the question

  • Reads (?!...) as a non-capturing group rather than a negative lookahead
  • Thinks _next/static requests bypass middleware automatically
  • Believes the exclusions are only about correctness, not per-request cost
  • Assumes middleware invocations scale with page views, not asset requests
  • Writes the matcher without the leading slash or without escaping the dot in extensions

context