Next.js Middleware Guide: Edge Runtime, Authentication, and Request Rewrites
In modern full-stack web applications, intercepting HTTP requests before they complete rendering or reach route handlers is vital for security, localization, and analytics. In Next.js, Middleware acts as a programmable request filter operating on the Edge Network—enabling developers to validate credentials, rewrite paths, and inject headers with low latency.
In this guide, we will explore the architecture of Next.js Middleware, how it runs on the Edge Runtime, implement practical authentication guards, handle rewrites and redirects, and discuss performance best practices.
1. What is Next.js Middleware?
Middleware allows you to run code before a request is completed. Based on the incoming request, you can modify the response by rewriting, redirecting, modifying request or response headers, or responding directly with custom HTML/JSON payloads.
Unlike traditional Node.js server middleware (such as Express.js middleware), Next.js Middleware runs in the Edge Runtime—a lightweight JavaScript runtime optimized for low latency and high concurrency located geographically close to the user.
2. File Convention and Execution Order
To declare Middleware in your Next.js project, create a single middleware.ts (or .js) file in the root directory of your project (or inside the src/ directory if you use one).
Basic Middleware Skeleton
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.next();
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
};
3. Key Practical Use Cases
A. Authentication and Protection Guards
Protecting private dashboard routes is one of the most widespread uses for middleware. By intercepting incoming requests, you can verify JWT cookies before page rendering starts:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth_token')?.value;
const { pathname } = request.nextUrl;
// Protect /dashboard routes
if (pathname.startsWith('/dashboard') && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
B. Header Manipulation (Security & Context)
You can inject custom request headers to pass contextual data (like user IDs or geo-location) to downstream App Router components without re-parsing cookies in every route:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Clone request headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-region', request.geo?.country || 'US');
// Return response with modified request headers
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
C. URL Rewriting for Multi-Tenant Apps
Rewriting allows you to serve a different path under the hood while keeping the user's address bar unchanged—ideal for multi-tenant SaaS subdomains:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host');
const subdomain = hostname?.split('.')[0];
if (subdomain && subdomain !== 'www' && subdomain !== 'localhost:3000') {
// Rewrite path to /tenants/[subdomain] internally
return NextResponse.rewrite(
new URL(`/tenants/${subdomain}${request.nextUrl.pathname}`, request.url)
);
}
return NextResponse.next();
}
4. Optimizing Path Matching via `matcher`
Middleware triggers on every request in your application by default (including static images, CSS bundles, and JS chunks). Filtering unnecessary executions using a regex matcher is critical for maintaining high performance.
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
5. Middleware vs. Traditional Node.js Server Architecture
| Dimension | Next.js Edge Middleware | Traditional Server Middleware |
|---|---|---|
| Execution Environment | Edge Runtime (V8 isolates near users) | Node.js Server Runtime (Centralized server) |
| Cold Start & Latency | Near-zero cold start (~1-5ms response) | Higher latency depending on geographic region |
| API Compatibility | Standard Web APIs (Fetch, Request, Response) | Full Node.js API support (fs, child_process, native C++) |
| Primary Purpose | Authentication, geo-routing, header management | Heavy computation, DB queries, file uploads |
Conclusion
Next.js Middleware provides a powerful boundary layer for intercepting and handling HTTP requests at the edge. By utilizing standard Web APIs, configuring efficient route matchers, and reserving heavy operations for backend Route Handlers or Server Actions, you can enforce security and manage routing seamlessly with minimal latency.
Happy Web Engineering! 🚀
Comments
Post a Comment