Introduction
Next.js Middleware executes before every request, running at the Edge or serverless region. A single middleware.ts file at the project root intercepts incoming requests and can redirect, rewrite, manipulate headers, or perform authentication checks before the request reaches the matched route.
Middleware is ideal for fast, edge-based decisions that should happen before page rendering. It does not run on static assets or _next/static by default.
Basic Structure
The middleware file exports a function that receives NextRequest and returns NextResponse, along with a config object that defines which paths trigger execution.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
console.log(`Request: ${request.nextUrl.pathname}`);
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
The matcher config uses path-to-regexp syntax with negative lookaheads to exclude static files. For section-specific middleware, use multiple patterns such as ['/dashboard/:path*', '/admin/:path*'].
Authentication and Authorization
Handling authentication in middleware is preferable to per-page checks because it provides a single source of truth and eliminates flickering on page transitions.
export function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
const { pathname } = request.nextUrl;
if (!token && pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (token && pathname.startsWith("/login")) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
For role-based access, decode the JWT or session token and check the user’s role before allowing access to admin routes. NextAuth.js, Clerk, and Auth0 all provide middleware helpers that simplify this process.
Request Rewriting and Redirects
Middleware supports two types of URL manipulation. Rewrites serve different content without changing the browser URL, while redirects change the URL entirely.
export function middleware(request: NextRequest) {
const { pathname, hostname } = request.nextUrl;
const tenant = hostname.split(".")[0];
if (tenant && tenant !== "www") {
request.nextUrl.pathname = `/${tenant}${pathname}`;
return NextResponse.rewrite(request.nextUrl);
}
}
Common use cases include localized landing pages, feature-gated content, and multi-tenant routing based on subdomain or cookie values.
Geolocation and Personalization
Edge middleware provides geolocation data through request.geo, which includes city, country, region, latitude, and longitude. The request.ip property gives the client IP address.
export function middleware(request: NextRequest) {
const { country } = request.geo;
const { pathname } = request.nextUrl;
if (["DE", "FR", "IT", "ES", "NL"].includes(country) && !pathname.startsWith("/gdpr")) {
return NextResponse.redirect(new URL("/gdpr", request.url));
}
return NextResponse.next();
}
Regional pricing, content localization, and GDPR consent routing are all practical applications of geolocation-based middleware logic.
Edge Runtime Limitations
Middleware runs on the Edge Runtime, which has important constraints:
| Limitation | Detail |
|---|---|
| Node.js APIs | Not available (no fs, crypto, path) |
| Max execution | 30 seconds (aim for under 1 second) |
| Bundle size | 1 MB limit (code plus dependencies) |
| Database | No direct connections; use API routes |
Keep middleware logic lean. Use API routes or server components for heavy computation, database access, or file system operations.
Conclusion
Use middleware for fast, edge-based decisions like authentication, redirects, and URL rewriting. Keep logic simple since it runs on every matching request. Prefer the matcher config over conditional checks inside the function for performance. Test middleware behavior separately from page logic using tools like Playwright or Cypress.
