NextjsMiddleware Matcher & Config

Middleware Matcher & Config

Left unconfigured, middleware.ts runs on every single request to your app — every page, every image, every font, every API call. That's rarely what you want. The exported config.matcher lets you restrict middleware to only the paths that actually need it.
Unscoped middleware has a real performance cost
Every request that hits an unscoped middleware — including requests for static assets like /favicon.ico or files under /_next/static — pays the cost of running your middleware function first. On a busy site this adds latency to traffic that middleware was never meant to touch. Always scope middleware with a matcher unless you genuinely need it on every path.
The matcher config
Export a config object alongside your middleware function with a matcher field — either a single path pattern or an array of them.

middleware.ts

TSX
import { NextRequest, NextResponse } from 'next/server'

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

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}
This runs the middleware only for /dashboard and /settings and everything nested under them, leaving the marketing homepage, blog, and static assets completely untouched by middleware.
Matcher syntax

Pattern

Matches

/about

Exactly /about, nothing nested under it

/dashboard/:path*

/dashboard and any depth of nested paths, e.g. /dashboard/settings/billing

/blog/:slug

Exactly one segment after /blog, e.g. /blog/hello-world but not /blog/a/b

/((?!api|_next/static|favicon.ico).*)

A regex-style negative-lookahead pattern — everything except the listed prefixes

The last pattern above is the common "run on every page but skip infrastructure paths" matcher, and is close to what the Next.js default template ships with when middleware needs broad coverage minus assets and API routes.

middleware.ts — excluding assets and API routes

TSX
export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}
Filtering inside the function instead
For logic too dynamic to express as a static matcher pattern (e.g. "skip if this specific cookie is present"), you can also check request.nextUrl.pathname inside the middleware function itself and return early with NextResponse.next(). Combining a broad matcher with an early return inside the function is a normal pattern.

middleware.ts — conditional early return

TSX
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/public')) {
    return NextResponse.next()
  }

  // ...auth check for everything else the matcher covers...
}
Tip
Prefer the narrowest matcher that covers what you need. It's far easier to reason about "this middleware only ever touches /dashboard/*" than to remember every early-return condition buried inside a function that technically runs on every request.
Note
The matcher is evaluated statically at build time to generate an efficient routing manifest, so it must be a literal array of strings in the config export — not a value computed at runtime.
  • Without a matcher, middleware runs on every request, including static assets — a real performance cost at scale.

  • config.matcher accepts one path or an array of path patterns (:path* for nested segments, :slug for a single segment, or a regex-style pattern).

  • A common broad pattern excludes api, _next/static, _next/image, and favicon.ico while covering everything else.

  • You can combine a matcher with an early NextResponse.next() return inside the function for logic too dynamic for a static pattern.

  • Scope middleware as narrowly as the use case allows.