Next.js Middleware: What It Does and How to Use It Correctly
A deep-dive into Next.js Middleware — how it works at the edge, what it is and is not suited for, and best practices for auth, redirects, and request rewriting.
Next.js Middleware is one of those features that looks simple and turns out to be surprisingly sharp once you deploy it at scale. It runs before a request reaches your page or API route — on the Edge Runtime, close to the user — and it can read the request, modify the response, redirect, or rewrite the destination. When used well, it handles authentication guards, locale detection, and A/B testing in a single function with near-zero latency. When misused, it adds latency to every request and creates subtle bugs that are hard to reproduce.
This guide covers what middleware actually is, what the edge runtime constrains, and the practices that keep it correct in production.
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 is the canonical auth guard: read the session cookie, redirect unauthenticated users to /login, let everyone else through.
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, the most common cause is a dependency that is 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.
Middleware is a clean solution to a specific set of problems: routing logic that needs to run before the page, close to the user, without a cold start. It works best when kept focused and fast.
If your team is building authentication flows, multi-tenant routing, or internationalization on Next.js and needs experienced engineering support, talk to Clixo. We build full-stack Next.js systems that hold up in production.