Skip to content

Middleware and the Request Lifecycle

A middleware.ts file at the root of your project runs BEFORE a request finishes, on every matching route, so you can redirect, rewrite, or set headers and cookies before Next.js renders anything.

Where middleware sits in the request lifecycle

Section titled “Where middleware sits in the request lifecycle”

Middleware intercepts the request after it arrives but before the route is resolved. You return a NextResponse to redirect, rewrite, or continue with NextResponse.next().

// middleware.ts — runs BEFORE the request completes, on every matching route
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Lightweight auth gate: redirect logged-out users away from /dashboard
const isLoggedIn = request.cookies.has('session')
if (request.nextUrl.pathname.startsWith('/dashboard') && !isLoggedIn) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Otherwise continue, and set a header on the way through
const response = NextResponse.next()
response.headers.set('x-request-time', Date.now().toString())
return response
}

Without a matcher, middleware runs on every request — including static assets. Scope it so it only runs where you need it. The matcher config accepts path patterns and can exclude internals.

// middleware.ts — scope which routes run middleware
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

Use middleware for redirects, rewrites (same URL, different content), and setting headers or cookies. A rewrite serves different content without changing the browser URL — useful for A/B tests or geo routing.

// A/B or geo rewrite — same URL, different content served
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.headers.get('x-vercel-ip-country') ?? 'us'
return NextResponse.rewrite(new URL(`/${country}${request.nextUrl.pathname}`, request.url))
}

Middleware runs on the Edge runtime by default. It sits in front of every matching request, so keep it fast: no database calls, no heavy computation, no large dependencies. Do the lightweight decision here and push real work to the route.

graph LR
  A["Incoming request"] --> B["middleware.ts (Edge)"]
  B -->|redirect| C["NextResponse.redirect"]
  B -->|rewrite| D["NextResponse.rewrite"]
  B -->|next| E["Route handler / page"]
Middleware intercepts before the route resolves
When does middleware.ts run?
What is the matcher config used for?
What runtime does middleware use by default?
Which of these belongs in middleware?