Middleware
AdvancedCode that runs before every matched request, on the edge, with no database and a tight time budget. Right for redirects and coarse gates; wrong as your authorisation layer.
Overview
Middleware sits in front of matched routes and can rewrite, redirect, or attach headers before anything renders. Because it runs on every request in that scope, it is the highest-leverage and most easily abused file in a Next application: a slow middleware slows the entire site, and one that tries to do real authorisation gives a false sense of security. The 2025 CVE that allowed middleware to be bypassed with a crafted header made that concrete — treat it as routing, and keep the real checks next to the data.
Shape and Matcher
One file at the root, and the matcher that decides where it runs.
// middleware.ts — at the project root, beside app/
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const session = req.cookies.get('sid')
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
const url = new URL('/login', req.url)
url.searchParams.set('from', req.nextUrl.pathname) // come back after
return NextResponse.redirect(url)
}
const res = NextResponse.next()
res.headers.set('x-request-id', crypto.randomUUID())
return res
}
// The matcher is the performance control. Without it, middleware runs
// for every request including static assets.
export const config = {
matcher: [
'/dashboard/:path*',
'/admin/:path*',
// or everything except assets and images:
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
// It also runs on prefetches, so a redirect here fires more often
// than you expect — keep it cheap.The Constraints
Edge runtime: no Node APIs, no database, and a hard time budget.
// Runs on the edge runtime, so these are unavailable:
// fs, net, most Node built-ins
// a Prisma or pg client (a TCP connection)
// bcrypt and other native modules
// heavy libraries — the bundle has a size cap
// Which means you CANNOT look up a session in the database here. The
// usual shape is a stateless check only:
const token = req.cookies.get('token')?.value
if (!token) return NextResponse.redirect(new URL('/login', req.url))
// Verify a JWT signature with jose (edge-compatible) if you need more,
// and accept that revocation is not visible here.
// Keep it under a few milliseconds. Every request in the matcher pays
// this cost, on every page view, forever.
// Other things it can do:
NextResponse.rewrite(new URL('/maintenance', req.url)) // URL unchanged
res.cookies.set('locale', 'en-IN')
req.headers.get('x-forwarded-for') // geo, A/B, bot checks
// Things it should not do: call your API, log to a slow sink, run
// business logic, or make a decision that needs fresh data.Not an Authorisation Layer
The most important point in this concept.
// Middleware is a redirect for convenience — it stops a signed-out
// user landing on a dashboard shell. It is NOT the check that keeps
// data safe.
// CVE-2025-29927 allowed middleware to be skipped entirely with a
// crafted x-middleware-subrequest header. Applications that relied on
// it as their only gate were exposed. Patch releases fixed the bypass;
// the architectural lesson stands.
// So authorise where the data is read or written:
// server component -> check before querying
// server action -> check before mutating
// route handler -> check before responding
const user = await requireUser() // throws or redirects
const rows = await db.thing.findMany({ where: { userId: user.id } })
// The Data Access Layer pattern makes this hard to forget: every read
// goes through a function that takes the session, so a query written
// without one does not compile.
// Use middleware for:
// redirecting unauthenticated users away from an app shell
// locale and geo routing
// A/B assignment, maintenance mode, request ids
// Never for:
// deciding who may see which recordKey Points to Remember
- 1Middleware runs before matched requests on the edge — the matcher is what keeps it off static assets
- 2The edge runtime has no Node built-ins and no database access, so session lookups cannot happen here
- 3It runs on prefetches too, so it must stay within a few milliseconds
- 4A 2025 CVE allowed middleware to be bypassed with a crafted header — never rely on it as the only gate
- 5Authorise where data is read or written; a Data Access Layer makes that hard to forget
Interview Questions
Sign in to ask AriaWhat can middleware not do, and why?
Why is middleware the wrong place for authorisation?
What does the matcher config affect besides correctness?
Ask Aria about Middleware
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.