skip to content

In a Next.js App Router project, where must the middleware file live, and what does exporting `export const config = { matcher: [...] }` from it change about when the middleware function runs?

level: juniorimportance: must knowfreq 65%

answer

  1. one file, one project
  2. root level, beside app
  3. no matcher means everything
  4. entries must start with a slash
  5. framework filters before your code runs

basics

~20 s

Next.js reads a single middleware file at the project root, next to app/ (or inside src/). Exporting config.matcher restricts which request paths invoke it; with no matcher, the middleware function runs for every request the app serves.

solid answer

~50 s

Next.js supports one middleware file per project: `middleware.ts` (or `.js`) at the repository root alongside `app/`, or inside `src/` when your code lives there. It is not a per-segment convention — dropping the file inside `app/dashboard/` does nothing. The module exports a function named `middleware` (or a default export) that receives a `NextRequest` and runs before the request is routed. By default that means *every* request: page navigations, route handlers, the RSC payload fetches that client navigations issue, and static asset requests. `export const config = { matcher: [...] }` narrows that set. Each entry is a path pattern that must begin with `/`, and it is matched against the incoming URL pathname, not against your route files. Only a request whose pathname matches at least one entry invokes the function — the framework filters before your code runs, so an unmatched request never enters middleware at all.

code

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

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  response.headers.set('x-pathname', request.nextUrl.pathname)
  return response
}

export const config = {
  matcher: ['/dashboard', '/account'],
}

go deeper

for a junior

Be able to say where the file goes — project root beside app/, or inside src/ — and that without a matcher it runs for every request. Know that config.matcher is an array of URL path patterns starting with a slash.

for a middle

Explain that the matcher is applied by the routing layer before your module executes, that entries are matched against the request pathname rather than route files, and why that is cheaper than an early return inside the function.

for a senior

Show that you know what the un-matched default really covers in production traffic — assets, API calls, RSC payload and prefetch requests — and that you would treat the matcher as the primary control on how often middleware runs at all.

for a principal

Own the matcher as a reviewed piece of configuration, not an afterthought: it defines both the cost and the blast radius of the one middleware a project gets, and a route added later that silently falls outside it is a real failure mode to plan for.

## One file, one place Next.js looks for a single middleware module at the top level of the project: `middleware.ts` or `middleware.js`, sitting beside the `app/` directory, or inside `src/` if your application code lives under `src/`. It is deliberately **not** a per-segment file convention. `layout.tsx`, `loading.tsx`, `error.tsx` and `page.tsx` nest with the route tree; middleware does not. Putting a `middleware.ts` inside `app/dashboard/` gives you a file Next.js will simply ignore. One project, one middleware module — any per-area behaviour is composition you write yourself inside that module. The module has two meaningful exports: ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { return NextResponse.next() } export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], } ``` The function (exported as `middleware`, or as the module's default export) is the handler. `config` is metadata about *when* to call it. ## What happens with no matcher If you omit `config` entirely, the middleware is invoked for every route in the project. That is broader than most people picture, because middleware sits ahead of the filesystem in Next's request pipeline. Requests for hashed build output under `/_next/static`, requests to the image optimization endpoint under `/_next/image`, requests to route handlers under `app/api`, and the RSC payload requests that `next/link` issues when the user navigates client-side or when a link is prefetched — all of them are HTTP requests with a pathname, and all of them reach middleware unless a matcher excludes them. That is why the un-matched default is rarely what you want. A single-page navigation on a real app can produce a page document request plus a dozen asset requests, and each matched one is a separate middleware invocation on the critical path. ## What the matcher actually does `config.matcher` is either one pattern string or an array of them. Semantics worth being precise about: - **Entries are a union.** A request that matches any entry is passed to the function; the function itself has to work out which concern applies. - **Patterns must start with `/`.** They describe URL paths, not filenames. - **Matching is against the request pathname**, not against the route that would eventually resolve. Middleware runs before route resolution, so at that moment there is no matched route and no dynamic-segment params — only a URL. - **Patterns support more than literals.** Beyond exact paths such as `/dashboard`, an entry can contain a regular-expression group, which is how the canonical exclusion pattern `'/((?!_next/static|_next/image|favicon.ico).*)'` is written. ## Matcher versus an `if` inside the function You can always write `if (!request.nextUrl.pathname.startsWith('/dashboard')) return NextResponse.next()` at the top of your function, and functionally it looks the same. It is not the same operationally. With a matcher, the router decides not to invoke middleware at all; with an early return, the middleware module has already been loaded and executed for that request. On a deployment where middleware is a separate invocation in front of your app, the difference is the invocation itself — its cold-start risk, its latency, and on many hosts its billing. Scope with the matcher first; use in-function checks only for conditions a path pattern cannot express. ## The mistakes that show up in review - Assuming middleware only sees page navigations. It sees asset requests, API requests and prefetches too. - Expecting several middleware files to compose by folder depth. There is one. - Writing matcher entries without the leading slash, or writing them as route-file paths (`/app/dashboard/page`) rather than URL paths (`/dashboard`). - Trying to read a dynamic route param inside middleware. Routing has not happened yet; you have `request.nextUrl.pathname` and you parse it yourself. - Leaving the matcher off during development because "it works", then discovering in production that every static asset is going through the function. The mental model that keeps all of this straight: the matcher is a guest list checked at the door by the framework, and the middleware function is what happens once you are inside. Anything not on the list never gets to the door.

  • Could you add a second middleware file inside a route folder so it only applies to that subtree?
    No. Middleware is not a nested file convention like layout or loading — Next.js reads one middleware module, at the project root or under `src/`. A file placed inside a route folder is ignored. Per-subtree behaviour is written as branching inside the single function, scoped as tightly as the matcher allows.
  • If a request's pathname does not match the matcher, does the function run and return early?
    It does not run at all. The matcher is applied by the framework's routing layer before the middleware module is invoked, so an unmatched request never enters your code. That is the difference between a matcher and an early `return` on `request.nextUrl.pathname` inside the function, which still pays for the invocation.
  • Can middleware read a dynamic segment's parameter, such as the id in /products/[id]?
    Not directly. Middleware runs before route resolution, so no route has been matched and no params object exists yet. You get the raw URL through `request.nextUrl`, and if you need the id you parse the pathname yourself. Anything that genuinely depends on the resolved route belongs in the page, layout, or route handler.

The matcher is the guest list checked at the door by the venue, and the middleware function is what happens to you inside. A name that is not on the list never reaches the door at all.

saying these in an interview costs you the question

  • Thinks middleware.ts can be nested per route segment like layout.tsx
  • Believes middleware only runs for page navigations, not assets or API routes
  • Expects dynamic route params to be available inside middleware
  • Writes matcher entries without a leading slash or as file paths
  • Assumes an early return in the function costs the same as excluding the path

context