Next.js Middleware: Auth, Redirects and Edge Use Cases
Middleware runs before every matched request, which makes it powerful and easy to misuse. Where it shines, where it does not, and how to keep it fast.
Middleware is the only code in a Next.js app that runs before routing: one function, executed for every request that matches its config, able to rewrite, redirect, or stamp headers before a page or asset is served. Used well it handles cross-cutting concerns in one place. Used carelessly it becomes a tax on every single request. The difference is knowing what belongs there.
How middleware runs
The middleware file sits at the project root and exports a single function. It typically runs on the Edge runtime — a trimmed-down environment without Node APIs — and it sits on the critical path of every matched request, so both constraints shape what you should do in it: quick checks on cookies, headers and URLs, not database queries or heavy computation.
Header manipulation is the quiet superpower here. Middleware can stamp a request id for tracing, attach security headers to every response, or forward a normalised tenant identifier so downstream code never has to re-derive it. Because it runs before everything, it is the one place such cross-cutting concerns can live without duplication.
Auth gating without a round trip
The classic use case is keeping unauthenticated visitors out of an app shell. Check for the session cookie and redirect to the login page, preserving the intended destination.
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session');
if (!session) {
const login = new URL('/login', request.url);
login.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(login);
}
return NextResponse.next();
}Treat this as an optimistic check, not authorisation. Middleware confirms a cookie exists; it should not be the only thing verifying the session or deciding what a user may access. Real authorisation belongs next to the data — in the page, layout or data layer — with middleware providing the fast early redirect. And be exhaustive about public paths — login, marketing pages, webhook endpoints — or you will build a redirect loop on your first deploy.
Redirects, rewrites and experiments
Beyond auth, middleware earns its keep wherever a request needs rerouting before rendering:
- Bulk redirects after a migration, driven by a lookup rather than hundreds of config entries
- A/B tests: assign a variant cookie and rewrite to a variant route, invisibly to the user
- Geo or language routing based on request headers before the page renders
- Rewriting vanity URLs or multi-tenant domains onto internal route structures
Rewrites are the underrated half. A rewrite changes which route renders without changing the URL in the address bar, which is exactly what tenant subdomains and experiments need.
Scope it with a matcher
By default middleware runs on everything, including prefetches and static assets. Always narrow it.
export const config = {
matcher: [
'/dashboard/:path*',
'/account/:path*',
],
};Know the limits
On the Edge runtime there is no Node filesystem and most native modules will not load, so heavyweight libraries — ORMs especially — do not belong here. Keep the function to a few milliseconds, avoid network calls unless they are genuinely unavoidable, and remember that every byte of logic runs on every matched request. Middleware is a scalpel; if a job needs more than a scalpel, it belongs in a Route Handler or the page itself.
Where it runs depends on your platform, too: on Vercel it executes at the edge, close to the visitor, while on a self-hosted deployment it runs inside the Node server before the router. The code is identical; the latency profile is not, which occasionally changes whether a lookup inside middleware is acceptable.
If your Next.js routing has grown tangled — redirect loops, mystery rewrites, middleware doing too much — STRCLI can help untangle it; drop us a line.