Middleware
Middleware patterns for request interception, redirects, rewrites, authentication guards, and geo-routing in Next.js
You are an expert in Next.js Middleware for intercepting and transforming requests at the edge before they reach your application routes. ## Key Points - Keep middleware lightweight — it runs on every matched request and uses the Edge Runtime (limited Node.js APIs). - Use the `matcher` config to narrow which routes trigger middleware instead of checking `pathname` in the function body. - Pass computed data to route handlers and pages via request headers rather than duplicating logic. - Avoid importing heavy libraries; the Edge Runtime has a code size limit and does not support all Node.js modules. - Use `NextResponse.rewrite()` for A/B testing or feature flags — it serves different content without changing the URL. - **Middleware runs on every request**: Without a `matcher`, it intercepts static assets, images, and `_next/` requests. Always exclude these. - **Edge Runtime limitations**: `fs`, `child_process`, and many Node.js built-ins are unavailable. Use edge-compatible libraries (e.g., `jose` instead of `jsonwebtoken`). - **In-memory state is unreliable**: On serverless platforms each invocation may be a fresh instance. Do not rely on module-level `Map` or variables for persistent state. - **Middleware cannot access the body**: You cannot read `request.body` in middleware. Use Route Handlers for body inspection. - **Order of operations**: Middleware runs before `revalidate`, `redirect` in `next.config.js`, and route resolution. Conflicting redirects can cause loops.
skilldb get nextjs-skills/MiddlewareFull skill: 298 linesMiddleware — Next.js
You are an expert in Next.js Middleware for intercepting and transforming requests at the edge before they reach your application routes.
Overview
Next.js Middleware runs before every matched request. It executes on the Edge Runtime and can rewrite, redirect, set headers, or return responses directly. Middleware is defined in a single middleware.ts file at the root of the project (next to app/ or pages/).
Core Concepts
Basic Structure
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Runs before every matched route
return NextResponse.next();
}
// Only run on specific paths
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};
The NextRequest Object
Extends the standard Request with convenience properties:
export function middleware(request: NextRequest) {
request.nextUrl; // URL object with pathname, searchParams, etc.
request.cookies; // RequestCookies API
request.headers; // Standard Headers
request.geo; // { city, country, region } (Vercel only)
request.ip; // Client IP (Vercel only)
}
Response Types
// Continue to the route (optionally modify request headers)
NextResponse.next({ request: { headers: newHeaders } });
// Redirect to a different URL
NextResponse.redirect(new URL("/login", request.url));
// Rewrite — serve a different route without changing the URL
NextResponse.rewrite(new URL("/api/proxy", request.url));
// Return a response directly (short-circuit)
new NextResponse("Unauthorized", { status: 401 });
// Return JSON
NextResponse.json({ error: "Forbidden" }, { status: 403 });
Matcher Configuration
export const config = {
matcher: [
// Match specific paths
"/dashboard/:path*",
// Match all paths except static files and images
"/((?!_next/static|_next/image|favicon.ico).*)",
// Match API routes
"/api/:path*",
],
};
Core Philosophy
Middleware is the single entry point for cross-cutting request logic. Instead of scattering authentication checks, locale detection, feature flags, and security headers across individual pages, Middleware centralizes them in one file that runs before any route resolves. This architectural choice reflects the principle that request-level concerns should be handled at the request level, not inside the rendering layer.
The Edge Runtime constraint is intentional, not incidental. By limiting Middleware to the edge-compatible API surface — no filesystem access, no heavy Node.js modules — Next.js ensures that Middleware executes in under a millisecond at the CDN edge, close to the user. This forces you to keep Middleware lightweight: read a cookie, verify a JWT, set a header, redirect. If your logic is too complex for the Edge Runtime, it belongs in a Route Handler or Server Component, not in Middleware.
Middleware should transform requests, not generate responses. The idiomatic pattern is to inspect incoming requests, make routing decisions (redirect, rewrite, or continue), and attach computed metadata via headers for downstream consumption. When Middleware starts returning full HTML or JSON responses, it has crossed into Route Handler territory and should be refactored accordingly.
Implementation Patterns
Authentication Guard
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const publicPaths = ["/", "/login", "/signup", "/api/auth"];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow public paths
if (publicPaths.some((p) => pathname.startsWith(p))) {
return NextResponse.next();
}
// Check for session token
const token = request.cookies.get("session-token")?.value;
if (!token) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Role-Based Access Control
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { jwtVerify } from "jose";
const roleRoutes: Record<string, string[]> = {
"/admin": ["admin"],
"/dashboard": ["admin", "user"],
"/api/admin": ["admin"],
};
export async function middleware(request: NextRequest) {
const token = request.cookies.get("token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
const userRole = payload.role as string;
for (const [path, roles] of Object.entries(roleRoutes)) {
if (
request.nextUrl.pathname.startsWith(path) &&
!roles.includes(userRole)
) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
}
// Pass user info downstream via headers
const headers = new Headers(request.headers);
headers.set("x-user-id", payload.sub as string);
headers.set("x-user-role", userRole);
return NextResponse.next({ request: { headers } });
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
Rate Limiting
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const rateLimit = new Map<string, { count: number; resetAt: number }>();
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith("/api/")) {
return NextResponse.next();
}
const ip = request.ip ?? request.headers.get("x-forwarded-for") ?? "unknown";
const now = Date.now();
const window = 60_000; // 1 minute
const maxRequests = 60;
const entry = rateLimit.get(ip);
if (!entry || now > entry.resetAt) {
rateLimit.set(ip, { count: 1, resetAt: now + window });
return NextResponse.next();
}
if (entry.count >= maxRequests) {
return NextResponse.json(
{ error: "Too many requests" },
{
status: 429,
headers: { "Retry-After": String(Math.ceil((entry.resetAt - now) / 1000)) },
}
);
}
entry.count++;
return NextResponse.next();
}
Note: In-memory rate limiting only works for single-instance deployments. For production at scale, use an external store like Redis or Upstash.
Internationalization (i18n) Routing
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
const locales = ["en", "fr", "de", "ja"];
const defaultLocale = "en";
function getLocale(request: NextRequest): string {
const headers = Object.fromEntries(request.headers);
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if pathname already has a locale
const hasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return NextResponse.next();
// Redirect to locale-prefixed path
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
};
Adding Security Headers
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()"
);
response.headers.set(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload"
);
return response;
}
Best Practices
- Keep middleware lightweight — it runs on every matched request and uses the Edge Runtime (limited Node.js APIs).
- Use the
matcherconfig to narrow which routes trigger middleware instead of checkingpathnamein the function body. - Pass computed data to route handlers and pages via request headers rather than duplicating logic.
- Avoid importing heavy libraries; the Edge Runtime has a code size limit and does not support all Node.js modules.
- Use
NextResponse.rewrite()for A/B testing or feature flags — it serves different content without changing the URL.
Common Pitfalls
- Middleware runs on every request: Without a
matcher, it intercepts static assets, images, and_next/requests. Always exclude these. - Edge Runtime limitations:
fs,child_process, and many Node.js built-ins are unavailable. Use edge-compatible libraries (e.g.,joseinstead ofjsonwebtoken). - In-memory state is unreliable: On serverless platforms each invocation may be a fresh instance. Do not rely on module-level
Mapor variables for persistent state. - Middleware cannot access the body: You cannot read
request.bodyin middleware. Use Route Handlers for body inspection. - Order of operations: Middleware runs before
revalidate,redirectinnext.config.js, and route resolution. Conflicting redirects can cause loops.
Anti-Patterns
-
Running heavy computation or database queries in Middleware. Middleware executes on every matched request at the edge. Calling a database, invoking an external API, or performing expensive computation here adds latency to every page load. Middleware should read cookies, verify tokens, and make routing decisions — nothing more.
-
Using module-level
Mapor variables for persistent state. On serverless and edge platforms, each invocation may run in a fresh isolate. In-memory rate limiters, counters, or caches stored in module scope will be empty on the next request. Use an external store like Redis or Upstash for state that must persist across requests. -
Forgetting to exclude static assets from the matcher. Without
/((?!_next/static|_next/image|favicon.ico).*)in your matcher, Middleware runs on every CSS file, JavaScript chunk, and image request. This adds unnecessary latency to asset loading and can break caching behavior. -
Duplicating auth logic in Middleware and in every page. The purpose of Middleware is to centralize cross-cutting concerns. If you are checking the session in Middleware and then repeating the same check in every Server Component, you have redundant code. Use Middleware for route-level gating and per-component checks only for authorization (role-based access) that varies by page.
-
Trying to read the request body in Middleware. The Edge Runtime does not support reading
request.bodyin Middleware. If you need to inspect or validate the body of a POST request, do it in a Route Handler or Server Action. Attempting to read the body in Middleware will silently fail or throw.
Install this skill directly: skilldb add nextjs-skills
Related Skills
API Routes
Route Handlers for building REST APIs, handling webhooks, streaming responses, and CORS in Next.js App Router
App Router
App Router fundamentals including file-based routing, layouts, loading states, and parallel routes in Next.js
Authentication
Authentication patterns using NextAuth.js (Auth.js) and Clerk for protecting routes and managing sessions in Next.js
Data Fetching
Data fetching and caching strategies including Server Components, fetch options, ISR, and the Next.js cache layers
Deployment
Deployment strategies for Next.js including Vercel, self-hosting with Node.js, Docker containers, and static export
Image Optimization
Image optimization with next/image, asset handling, fonts, and performance tuning for Next.js applications