intermediate
Step 11 of 15
Middleware
Next.js Development
Middleware
Next.js middleware runs before a request is completed, allowing you to modify the response by rewriting, redirecting, setting headers, or even returning a response directly. Middleware runs on the Edge Runtime, which means it executes close to your users at CDN edge locations for minimal latency. Common use cases include authentication checks, internationalization, A/B testing, feature flags, bot detection, and request logging. Middleware is defined in a single middleware.ts file at the root of your project and can be configured to run on specific paths.
Basic Middleware
// middleware.ts (at the project root, next to app/ or pages/)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Log all requests
console.log(`[${request.method}] ${request.nextUrl.pathname}`);
// Add custom headers to the response
const response = NextResponse.next();
response.headers.set("x-request-id", crypto.randomUUID());
response.headers.set("x-request-path", request.nextUrl.pathname);
return response;
}
// Configure which paths the middleware runs on
export const config = {
matcher: [
// Match all paths except static files and API routes
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
Authentication Middleware
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// Paths that require authentication
const protectedPaths = ["/dashboard", "/settings", "/profile", "/admin"];
// Paths that should redirect authenticated users (e.g., login page)
const authPaths = ["/login", "/register"];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const token = request.cookies.get("auth-token")?.value;
const isAuthenticated = !!token;
// Protect authenticated routes
if (protectedPaths.some((path) => pathname.startsWith(path))) {
if (!isAuthenticated) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(loginUrl);
}
}
// Redirect authenticated users away from auth pages
if (authPaths.some((path) => pathname.startsWith(path))) {
if (isAuthenticated) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
}
// Role-based access control
if (pathname.startsWith("/admin")) {
// Decode token to check role (simplified example)
try {
const payload = JSON.parse(atob(token!.split(".")[1]));
if (payload.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/profile/:path*",
"/admin/:path*", "/login", "/register"],
};
Redirects and Rewrites
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Redirect old URLs to new ones
if (pathname === "/old-blog") {
return NextResponse.redirect(new URL("/blog", request.url), 301);
}
// Rewrite URL (URL stays the same, but serves different content)
if (pathname.startsWith("/api/v1/")) {
const newPath = pathname.replace("/api/v1/", "/api/v2/");
return NextResponse.rewrite(new URL(newPath, request.url));
}
// Geolocation-based routing
const country = request.geo?.country || "US";
if (pathname === "/" && country === "DE") {
return NextResponse.rewrite(new URL("/de", request.url));
}
// A/B testing with cookies
if (pathname === "/pricing") {
const variant = request.cookies.get("ab-variant")?.value;
if (!variant) {
const newVariant = Math.random() < 0.5 ? "a" : "b";
const response = NextResponse.rewrite(
new URL(`/pricing/${newVariant}`, request.url)
);
response.cookies.set("ab-variant", newVariant, { maxAge: 86400 });
return response;
}
return NextResponse.rewrite(new URL(`/pricing/${variant}`, request.url));
}
return NextResponse.next();
}
Rate Limiting
// Simple rate limiting with middleware
const rateLimit = new Map<string, { count: number; timestamp: number }>();
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/")) {
const ip = request.headers.get("x-forwarded-for") || "unknown";
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute window
const maxRequests = 100;
const record = rateLimit.get(ip);
if (record && now - record.timestamp < windowMs) {
if (record.count >= maxRequests) {
return NextResponse.json(
{ error: "Too many requests" },
{ status: 429 }
);
}
record.count++;
} else {
rateLimit.set(ip, { count: 1, timestamp: now });
}
}
return NextResponse.next();
}
Tip: Keep middleware lightweight since it runs on every matched request. The Edge Runtime has limitations — no Node.js APIs like fs or heavy npm packages. For complex logic, have middleware make a quick check (like reading a cookie) and defer heavy processing to API routes or Server Components.
Key Takeaways
- Middleware runs before requests are completed, enabling authentication, redirects, and header modification.
- It executes on the Edge Runtime for minimal latency, close to users globally.
- Use the
config.matcherexport to specify which paths middleware applies to. NextResponse.redirectchanges the URL;NextResponse.rewriteserves different content at the same URL.- Keep middleware lightweight — defer heavy processing to API routes or Server Components.