Skip to main content

Next.js Middleware

Answer

Middleware runs before a request is completed, allowing you to modify the response by rewriting, redirecting, modifying headers, or returning directly.

How Middleware Works

Creating Middleware

// middleware.ts (root of project)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
// Runs on every matched request
console.log("Request to:", request.nextUrl.pathname);

return NextResponse.next();
}

// Configure which paths trigger middleware
export const config = {
matcher: "/api/:path*",
};

Common Use Cases

// 1. Authentication
export function middleware(request: NextRequest) {
const token = request.cookies.get("token")?.value;

if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}

return NextResponse.next();
}

// 2. Redirects
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/old-page") {
return NextResponse.redirect(new URL("/new-page", request.url));
}
}

// 3. Rewrites (URL stays same, content from different path)
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/api/v1/users") {
return NextResponse.rewrite(new URL("/api/v2/users", request.url));
}
}

// 4. Adding headers
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("x-custom-header", "my-value");
return response;
}

Matcher Configuration

// Match single path
export const config = {
matcher: "/dashboard",
};

// Match multiple paths
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};

// Match with regex
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

// Skip static files and images
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};

Geolocation & IP

export function middleware(request: NextRequest) {
const country = request.geo?.country || "US";
const city = request.geo?.city || "Unknown";
const ip = request.ip || "unknown";

// Redirect based on location
if (country === "DE") {
return NextResponse.redirect(new URL("/de", request.url));
}

// Add location to headers for use in pages
const response = NextResponse.next();
response.headers.set("x-user-country", country);
return response;
}

Reading & Setting Cookies

export function middleware(request: NextRequest) {
// Read cookie
const theme = request.cookies.get("theme")?.value || "light";

const response = NextResponse.next();

// Set cookie
response.cookies.set("visited", "true", {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24, // 1 day
});

// Delete cookie
response.cookies.delete("old-cookie");

return response;
}

Chain of Checks

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;

// 1. Check maintenance mode
if (process.env.MAINTENANCE_MODE === "true") {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}

// 2. Check authentication for protected routes
const isProtected =
pathname.startsWith("/dashboard") || pathname.startsWith("/settings");
const token = request.cookies.get("auth-token");

if (isProtected && !token) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(loginUrl);
}

// 3. Add security headers
const response = NextResponse.next();
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");

return response;
}

Rate Limiting Example

const rateLimit = new Map();

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api")) {
const ip = request.ip || "anonymous";
const count = rateLimit.get(ip) || 0;

if (count > 100) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}

rateLimit.set(ip, count + 1);
setTimeout(() => rateLimit.delete(ip), 60000);
}

return NextResponse.next();
}

Key Points

  • middleware.ts at project root runs before requests
  • Use matcher to limit which paths run middleware
  • Can redirect, rewrite, set headers, or block
  • Access cookies, headers, geo, and IP
  • Runs at the Edge (fast, but limited APIs)
  • Use for auth, redirects, A/B testing, headers