skip to content

Middleware and API Routes

Middleware runs before a request reaches a route, and route handlers are the endpoints that replaced API routes in the App Router. Interviewers ask where a given piece of logic belongs, because auth checks in the wrong layer are both slow and unsafe.

part ofNext.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

26

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

open as a page

In Next.js middleware, what is the difference between returning NextResponse.rewrite(url) and NextResponse.redirect(url)?

level: juniorimportance: must knowfreq 78%

basics

~20 s

NextResponse.rewrite serves a different route internally: the browser keeps the original URL and makes one request. NextResponse.redirect returns a 3xx with a Location header, so the browser makes a second request and the address bar changes.

open as a page

In the Next.js App Router, how do you define an HTTP endpoint in a route.ts file, and what happens when a request arrives with a method the file does not export?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A route.ts file turns its folder's path into an HTTP endpoint. You export one async function per HTTP method, named exactly GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS, and Next.js answers 405 Method Not Allowed for any method you did not export.

open as a page

A Next.js App Router app can run server code in middleware.ts, in a route handler at app/api/.../route.ts, and in a Server Action marked with 'use server'. What decides which of the three a given piece of server logic belongs in?

level: middleimportance: must knowfreq 72%

basics

~20 s

Who calls the code decides. Middleware handles cross-cutting work on every matched request before routing; a route handler is a public HTTP endpoint for callers outside your app; a Server Action is an internal mutation invoked by your own UI.

open as a page

In a Next.js App Router project, importing a Node-only library such as a TCP database driver into `middleware.ts` fails the build. Which runtime does middleware execute in, and what does that runtime not provide?

level: middleimportance: must knowfreq 72%

basics

~20 s

Next.js middleware runs in the Edge Runtime, a V8 isolate exposing only Web-standard APIs such as fetch and Web Crypto. Node built-ins like fs, net and tls, and packages with native addons, are absent, so a TCP database driver cannot bundle.

open as a page

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%

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.

open as a page

In Next.js middleware you compute a value — say a request ID — that the Server Component handling the same request needs. How do you pass it down, and why doesn't setting it on request.headers work?

level: middleimportance: must knowfreq 55%

basics

~20 s

Clone the incoming headers, set your value on the clone, and pass it through NextResponse.next({ request: { headers: cloned } }). The route then reads it with await headers(). The incoming request.headers object is not the copy Next forwards inward, so mutating it changes nothing.

open as a page

In a Next.js app, middleware reads a role claim from the session cookie and admits only role=admin to /admin/*, and the team now treats every page and Server Action under /admin as trusted. Which authorization decisions can that middleware check not make, and where do you enforce them instead?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Middleware sees a URL, headers and cookies before the route resolves, so it can gate a route family but cannot decide whether this user may read this record, and its role claim is a snapshot that survives revocation. Enforce per-resource authorization in the code that queries the data.

open as a page

A Next.js team wants per-request logic that needs a Node-only SDK and a database lookup, and the logic feels like it belongs in `middleware.ts`. How would you restructure it so it can actually run?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Split the work by runtime. Keep in middleware only what the Edge Runtime can do from the request itself, and move the Node SDK and database lookup into a route handler, Server Component or Server Action running the Node.js runtime, which is their default.

open as a page

In the Next.js App Router, how can you tell whether a GET route handler runs on every request or is prerendered and cached at build time, and what decides which one you get?

level: seniorimportance: must knowfreq 58%

basics

~20 s

In Next.js 15 and 16 a GET route handler runs on every request unless you opt in with export const dynamic = 'force-static'; Next.js 14 and earlier cached them by default. The build output labels each route static or dynamic.

open as a page

In a Next.js App Router app, middleware sends a signed-out visitor from /dashboard/settings to /login. How do you get that visitor back to /dashboard/settings after they sign in, and what must you check about the stored destination before you redirect to it?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Store the original path in a query parameter on the login URL: read it from request.nextUrl in middleware, redirect to /login?callbackUrl=/dashboard/settings, then after sign-in redirect back only if that value is a relative, same-origin path.

open as a page

A payment provider needs to send webhook callbacks into your Next.js App Router application. Would you implement that endpoint as a Server Action or as a route handler, and why?

level: juniorimportance: should knowfreq 55%

basics

~20 s

A route handler. The provider needs a fixed URL, its own authentication, and a specific status code in reply; a Server Action has no published URL and is meant to be called only by your own application's UI.

open as a page

You need a SHA-256 hash of a cookie value inside `middleware.ts` in a Next.js app. Why does `import { createHash } from 'node:crypto'` not work there, and what do you use instead?

level: juniorimportance: should knowfreq 46%

basics

~20 s

Node's crypto module is not part of the Edge Runtime that Next.js middleware runs in. Use the Web Crypto global instead: encode the string with TextEncoder and await crypto.subtle.digest('SHA-256', bytes), then format the resulting ArrayBuffer yourself.

open as a page

Why must the values in a Next.js middleware `export const config = { matcher: [...] }` be literal constants, and what happens if you build one from a variable at runtime?

level: middleimportance: should knowfreq 28%

basics

~20 s

Next.js reads config.matcher by static analysis at build time rather than by executing the module per request, so matcher values must be literal constants. A value computed from a variable is ignored, and the scoping you intended never ships.

open as a page

Next.js lets you declare redirects(), rewrites() and headers() in next.config, or implement the same behaviour in middleware. How do you choose between them?

level: middleimportance: should knowfreq 48%

basics

~20 s

Use next.config when the rule is static and known at build time — it is declarative routing config with no JavaScript running per request. Use middleware when the decision needs per-request logic the config cannot express, such as a computed value, a lookup, or a generated nonce.

open as a page

In a Next.js App Router route handler, what does the NextRequest argument add on top of the standard Web Request, and how do you read a query-string parameter and a cookie from it?

level: middleimportance: should knowfreq 55%

basics

~10 s

NextRequest extends the Web Request, so request.json(), request.headers and request.formData() still work. It adds request.nextUrl, an already-parsed URL whose searchParams give query values, and request.cookies with get, getAll and has for reading cookies.

open as a page

How do you return a streamed response from a route handler in the Next.js App Router, and why can a response built with NextResponse.json() never stream?

level: middleimportance: should knowfreq 42%

basics

~20 s

Build a ReadableStream, enqueue encoded chunks as they become available, and return it as the body of a Response. NextResponse.json() serializes one complete value into a buffered body, so nothing can leave the server until the whole value exists.

open as a page

A team wants Next.js middleware to load the signed-in user's full profile from the database on every request, so that pages do not have to fetch it themselves. Why is that the wrong surface, and where does the work belong?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Middleware sits on the critical path of every request its matcher selects, so the query is paid even by requests that never use the profile, and it has no way to hand an object to the page. Fetch the profile in the server component tree instead.

open as a page

In a Next.js App Router app, one "cancel subscription" operation must be triggered both by a form in your own UI and by a partner's server over HTTP. How do you structure the code so the business rule is implemented only once?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Put the rule in a plain server module that knows nothing about HTTP, then make the Server Action and the route handler thin adapters over it. Each adapter authenticates its own caller, validates its own input, and shapes its own response.

open as a page

Next.js middleware executes on every request it is applied to, before the response is produced. What size and CPU limits does that impose on it, and how do you stay inside them?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Middleware is one bundle on the hot path, so hosts cap its bundled size and give each invocation a short CPU budget. Keep dependencies tiny and edge-native, avoid embedding data files, and never do heavy computation or slow awaits there.

open as a page

After adding a `middleware.ts` to a Next.js app, p95 TTFB rises on marketing pages that are fully prerendered at build time. Why does a prerendered page still pay for middleware, and how would you bring the cost down?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Middleware runs before filesystem routes and before cached or prerendered content is served, so a static page still pays a middleware hop on every matched request. Narrow the matcher to the paths that genuinely need it and keep the function's work minimal.

open as a page

A multi-tenant Next.js app serves acme.example.com and globex.example.com from one deployment, using middleware to rewrite each request to /tenants/<tenant>/... . What does the rewrite buy you, and what must the app account for because the browser never sees the internal path?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The rewrite lets one route tree serve every tenant while each keeps its own clean URLs, since the internal /tenants/<tenant> prefix stays server-side. The catch is that only middleware knows the tenant, so links, redirects and any generated URLs must be written in the public space, not the internal one.

open as a page

You own authentication for a Next.js App Router codebase that a dozen product teams add routes and Server Actions to. How do you decide what the middleware gate is responsible for versus what each feature must enforce, and how do you keep an unprotected route from shipping?

level: principalimportance: should knowfreq 36%

basics

~20 s

Make middleware a deny-by-default gate for signed-in-ness only, and route every data access through one session-verifying module. Keep routes safe by construction — a negative matcher, an enforced import boundary, and tests that enumerate routes — not by asking teams to remember.

open as a page

A Next.js app wants several cross-cutting concerns in middleware — locale detection, a feature-flag cookie, and bot filtering — but a project gets only one middleware file. How do you decide how broad the `config.matcher` should be?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A project has one middleware function, so matcher entries are a union feeding it. Choose breadth by what must never be missed versus per-request cost and blast radius: exclusion-based patterns for exhaustive concerns, an allow-list for narrow ones.

open as a page