Next.js Middleware: What It Does and How to Use It Correctly
How to scope Next.js request interception for redirects, rewrites, locale routing, and authentication checks, with matcher and runtime considerations.
Next.js request interception can read cookies and headers, redirect a request, or rewrite its destination before a matched route runs. Keep this layer focused: work added here affects every matching request. Runtime support and file conventions depend on the Next.js version.
What Middleware Is
A middleware.ts (or middleware.js) file at the root of your project exports a function that Next.js runs before completing any matched request. It has access to the incoming Request object and can return a Response — either the original (pass-through), a redirect, or a rewrite.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const isAuthenticated = request.cookies.get('session')?.value
if (!isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
}This example checks for a session cookie and redirects when it is absent. Cookie presence alone does not validate a session or authorize access.
The Edge Runtime: What This Means for Your Code
Middleware runs in the Edge Runtime, not Node.js. This is a lightweight, V8-based environment that starts faster and runs globally on CDN edge nodes — but it does not have access to all Node.js APIs.
What is not available in the Edge Runtime:
- The
fsmodule (no filesystem access) - Most native Node.js modules
- Most database clients that depend on Node.js primitives
This means middleware cannot query your database directly. It can read cookies, headers, and URL parameters. For any auth check that requires a database lookup — validating a JWT against a revocation list, or looking up a user's role — you have two options: use a stateless token (JWT with claims embedded) that can be verified in the edge without a database call, or proxy the check through an API route.
Writing a Good Matcher
The config.matcher field controls which routes trigger the middleware function. Without a matcher, middleware runs on every request — including _next/static files, images, and fonts. This is almost never what you want.
Exclude static assets explicitly:
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}This pattern matches all routes except Next.js static files and common image extensions. Middleware running on static asset requests adds latency without benefit.
For auth guards, prefer matching only the routes that need protection:
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}Authentication Guards
Middleware is the right place for route-level authentication guards — it prevents unauthenticated users from ever reaching a protected page, even briefly. But it is not a substitute for server-side auth checks.
A user who bypasses your redirect (for example, by disabling JavaScript or using a direct API call) could still reach the page if the page itself does not verify authentication. Middleware is a first layer; server-side verification in the page or API route is the enforcing layer.
Best practice: Use middleware to redirect unauthenticated users. Use server-side auth checks in protected pages and API routes to enforce access control. Neither layer alone is sufficient.
Redirects and Rewrites
Redirects send the user to a different URL with an HTTP redirect status (307 for temporary, 308 for permanent). The browser navigates to the new URL.
Rewrites serve content from a different URL without changing what the browser displays. This is useful for:
- Proxying requests to an external API while keeping the origin hidden
- Serving different content to different user segments without a visible URL change
- A/B testing: routing a percentage of traffic to an experimental page without a redirect
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('ab-bucket')?.value
if (bucket === 'experiment') {
return NextResponse.rewrite(new URL('/experiment/home', request.url))
}
return NextResponse.next()
}The user sees /, but users in the experiment bucket are served content from /experiment/home.
Locale Detection
Middleware is the standard location for locale detection and internationalization routing. Read the Accept-Language header, determine the user's preferred locale, and redirect or rewrite to the locale-specific route.
Next.js has built-in i18n routing support in next.config.js, but custom middleware gives you more control: cookie-based locale preference, URL-based detection, and fallback logic.
What Middleware Should Not Do
Heavy computation. Middleware runs on every matched request. Any logic that adds meaningful latency compounds at scale. Keep it fast — cookie reads, header checks, simple redirects.
Database queries. The Edge Runtime cannot use most database clients. Attempts to query a database from middleware will fail at runtime or require a full network round-trip to an API route, negating the edge performance advantage.
Business logic. Middleware is an infrastructure layer. Routing, auth checks, and request augmentation belong here. Business rules belong in server components, Server Actions, or API routes.
Stateful operations. Middleware is stateless. It cannot accumulate state across requests.
Debugging Middleware
Middleware runs differently in development (next dev) than in production. In development, it runs as a Node.js function with some edge simulation. In production, it runs in the actual Edge Runtime.
If your middleware works locally but breaks in production, check for dependencies that are not edge-compatible. Check your imports against the list of supported packages for the Edge Runtime.
Log strategically — console.log output from edge middleware appears in your deployment platform's edge function logs, not in the Next.js server logs.
Use request interception for focused routing decisions. Test matchers, dependency compatibility, and latency on the deployment platform.
For help reviewing Next.js routing or authentication boundaries, contact LinkIT.