Middleware
Middleware runs before every matched request, enabling redirects, rewrites, header modifications, and authentication checks at the edge.
Recipe
Quick-reference recipe card — copy-paste ready.
// middleware.ts (root of project, same level as app/)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Redirect, rewrite, or modify headers
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};When to reach for this: Authentication guards, locale detection, A/B testing, redirects, and any request-level logic that must run before rendering.
Working Example
// middleware.ts — Auth guard with locale detection
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const protectedPaths = ["/dashboard", "/settings", "/billing"];
const defaultLocale = "en";
const supportedLocales = ["en", "fr", "de", "es"];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Locale detection — redirect if no locale prefix
const pathnameHasLocale = supportedLocales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!pathnameHasLocale) {
const locale =
request.headers.get("accept-language")?.split(",")[0]?.split("-")[0] ?? defaultLocale;
const detectedLocale = supportedLocales.includes(locale) ? locale : defaultLocale;
return NextResponse.redirect(
new URL(`/${detectedLocale}${pathname}`, request.url)
);
}
// 2. Auth check — redirect unauthenticated users
const isProtected = protectedPaths.some((path) =>
pathname.includes(path)
);
const token = request.cookies.get("session-token")?.value;
if (isProtected && !token) {
const loginUrl = new URL("/en/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
// 3. Add custom headers
const response = NextResponse.next();
response.headers.set("x-pathname", pathname);
return response;
}
export const config = {
matcher: [
// Match all paths except static files and Next.js internals
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};// middleware.ts — Simple redirect map
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const redirects: Record<string, string> = {
"/old-blog": "/blog",
"/docs/v1": "/docs",
"/legacy-api": "/api/v2",
};
export function middleware(request: NextRequest) {
const redirect = redirects[request.nextUrl.pathname];
if (redirect) {
return NextResponse.redirect(new URL(redirect, request.url), 301);
}
return NextResponse.next();
}
export const config = {
matcher: ["/old-blog", "/docs/v1", "/legacy-api"],
};Deep Dive
How It Works
- Middleware runs on every matched request before the route handler or page renders. It executes at the Edge Runtime by default.
- There is only one
middleware.tsfile per project. It must be at the root (next toapp/orsrc/). You cannot have per-route middleware files. - The
matcherconfig filters which routes trigger middleware. Without a matcher, middleware runs on every request including static assets. NextResponse.next()continues to the matched route. You can modify request/response headers while passing through.NextResponse.redirect(url)sends a redirect response (302 by default, pass 301 for permanent).NextResponse.rewrite(url)serves a different route's content without changing the browser URL.- Middleware runs in the Edge Runtime. Node.js APIs like
fs,path, and most npm packages are unavailable. Use only Web APIs. - Middleware can read and set cookies. Use
request.cookies.get()andresponse.cookies.set()for session management. - Middleware cannot render React components. It operates at the HTTP level, before any React rendering occurs.
Variations
// Rewrite-based A/B testing
export function middleware(request: NextRequest) {
const bucket = request.cookies.get("ab-bucket")?.value;
const response = NextResponse.next();
if (!bucket) {
const newBucket = Math.random() > 0.5 ? "a" : "b";
response.cookies.set("ab-bucket", newBucket, { maxAge: 60 * 60 * 24 * 30 });
}
const currentBucket = bucket ?? "a";
if (request.nextUrl.pathname === "/pricing") {
return NextResponse.rewrite(
new URL(`/pricing/${currentBucket}`, request.url)
);
}
return response;
}// Rate limiting with headers
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/")) {
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
// Check rate limit (using external store like Upstash Redis)
// This is pseudocode — use @upstash/ratelimit in production
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", "100");
return response;
}
return NextResponse.next();
}// Matcher patterns
export const config = {
matcher: [
"/dashboard/:path*", // /dashboard and all sub-paths
"/api/:path*", // All API routes
"/blog/:slug", // Single dynamic segment
"/((?!_next|static|api).*)", // Everything except _next, static, api
],
};TypeScript Notes
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// Middleware function signature
export function middleware(request: NextRequest): NextResponse | Response | undefined;
// NextRequest extends the Web Request API
// Key properties:
// request.nextUrl — parsed URL with pathname, searchParams
// request.cookies — RequestCookies instance
// request.headers — standard Headers object
// request.geo — { city, country, region } (Vercel only)
// request.ip — client IP (Vercel only)
// Config type
export const config: {
matcher: string | string[];
};Gotchas
- Only one middleware file per project. If you need different logic for different routes, use conditional checks on
request.nextUrl.pathnameinside a single middleware function. - Middleware runs on the Edge Runtime. You cannot use Node.js-specific APIs. Libraries like
bcrypt,fs, or database drivers will not work — use edge-compatible alternatives. - Middleware runs on static assets too unless you use a
matcher. Always configure a matcher to avoid unnecessary execution. - Infinite redirect loops are easy to create. If you redirect
/loginto/en/loginand middleware also matches/en/login, you get a loop. Exclude destination paths from the matcher. NextResponse.next()does not short-circuit. Other middleware logic after it still runs. Return early if needed.- Cookies set in middleware are available in Server Components via
cookies()fromnext/headers. - Rewrites are invisible to the client. The browser URL stays the same, but the server renders a different route. This can confuse client-side navigation if not handled carefully.
- Middleware cannot access the request body. It operates on headers, cookies, and URL only.
Alternatives
| Approach | When to Use |
|---|---|
next.config.js redirects | Static redirect rules that do not need runtime logic |
next.config.js rewrites | Static rewrite rules without conditional logic |
Route-level redirect() | Redirecting inside a Server Component after data checks |
| Server Actions with auth | Protecting mutations instead of routes |
| Layout-level auth guards | Checking auth in a layout and redirecting |
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: Middleware with rewrites, redirects, session refresh, and admin protection
// File: middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { updateSession } from '@/utils/supabase/middleware';
const legacyRedirects: Record<string, string> = {
'/old-docs': '/docs',
'/blog/legacy-post': '/articles',
'/pricing-old': '/pricing',
};
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Docs rewrite: serve /docs/[section] from a different internal route
if (pathname.startsWith('/docs/')) {
const url = request.nextUrl.clone();
url.pathname = `/docs-internal${pathname.replace('/docs', '')}`;
return NextResponse.rewrite(url);
}
// 2. Legacy redirects (301 permanent)
const redirect = legacyRedirects[pathname];
if (redirect) {
return NextResponse.redirect(new URL(redirect, request.url), 301);
}
// 3. Session refresh - keep Supabase auth session alive
const response = await updateSession(request);
// 4. Admin route protection
if (pathname.startsWith('/admin')) {
const session = request.cookies.get('sb-access-token')?.value;
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};What this demonstrates in production:
- Execution order matters. Rewrites run first, then legacy redirects, then session refresh, then admin protection. If you check auth before the rewrite, the user might hit the wrong route.
- The
301status code on legacy redirects tells search engines the move is permanent. Using the default302would preserve the old URL in search indexes. updateSession(request)is a Supabase utility that refreshes the auth session cookie on every request. This prevents the session from expiring while the user is actively browsing.- The matcher regex
/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)excludes static assets and images from middleware processing. Without this, middleware runs on every image and CSS file request, adding unnecessary latency. - Admin protection checks for a cookie rather than making a database call. Middleware runs on the Edge Runtime where database drivers are unavailable. Full authorization checks happen in Server Components or API routes.
- There is only one
middleware.tsfile per project. All route-specific logic must be handled with conditional checks onpathname.
FAQs
Where does the middleware.ts file go?
At the project root, next to the app/ or src/ directory. There is only one middleware.ts file per project. You cannot have per-route middleware files.
What runtime does middleware use and what are the limitations?
- Middleware runs on the Edge Runtime
- Node.js APIs like
fs,path, and most npm packages are unavailable - You can only use Web APIs
- You cannot access the request body
- You cannot render React components
What happens if you do not configure a matcher?
Middleware runs on every request, including static assets like images, CSS, and JavaScript files. Always configure a matcher to avoid unnecessary execution.
Gotcha: How do you avoid infinite redirect loops in middleware?
If you redirect /login to /en/login and middleware also matches /en/login, you get an infinite loop. Exclude destination paths from the matcher or add conditional checks to skip already-processed paths.
What is the difference between NextResponse.redirect and NextResponse.rewrite?
redirect()sends a redirect response (302 by default, 301 for permanent) and changes the browser URLrewrite()serves a different route's content without changing the browser URL
How do you read and set cookies in middleware?
export function middleware(request: NextRequest) {
// Read a cookie
const token = request.cookies.get("session")?.value;
// Set a cookie on the response
const response = NextResponse.next();
response.cookies.set("visited", "true", { maxAge: 3600 });
return response;
}Are cookies set in middleware available in Server Components?
Yes. Cookies set in middleware are available via cookies() from next/headers in Server Components.
How do you handle different logic for different routes in a single middleware file?
Use conditional checks on request.nextUrl.pathname inside the middleware function, since you can only have one middleware file.
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/api/")) { /* API logic */ }
if (pathname.startsWith("/admin")) { /* admin logic */ }
return NextResponse.next();
}What is the TypeScript signature for the middleware function?
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(
request: NextRequest
): NextResponse | Response | undefined;
export const config: {
matcher: string | string[];
};What properties are available on NextRequest?
request.nextUrl-- parsed URL with pathname and searchParamsrequest.cookies-- RequestCookies instancerequest.headers-- standard Headers objectrequest.geo-- city, country, region (Vercel only)request.ip-- client IP (Vercel only)
Gotcha: Does NextResponse.next() short-circuit the middleware function?
No. Code after NextResponse.next() still runs. If you want to stop processing, you must explicitly return the response.
When should you use middleware redirects vs. next.config.js redirects?
- Use
next.config.jsredirects for static rules that do not need runtime logic - Use middleware redirects when you need conditional logic based on cookies, headers, or other request data
Related
- App Router Basics — file conventions overview
- Navigation —
redirect()and programmatic navigation - Route Groups — organizing routes that middleware protects
- Dynamic Routes — matcher patterns for dynamic segments