|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/middleware
$cat docs/next.js-—-middleware.md
updated Last week·16 min read·published

Next.js — Middleware

Next.jsIntermediate🎯Free Tools
Introduction

Middleware runs before a request is completed. It allows you to modify the request and response, rewrite URLs, set headers and cookies, handle authentication, and implement A/B testing — all before the page renders. Middleware runs at the edge, close to the user, for minimal latency.

File Location & Basic Syntax
src/middleware.ts
TypeScript
1// middleware.ts MUST be at the root of your project (or in /src)
2// It runs for ALL routes by default — use the matcher to limit it
3
4// src/middleware.ts (or middleware.ts at project root)
5import { NextResponse, type NextRequest } from "next/server";
6
7export function middleware(request: NextRequest) {
8 // Runs before every matched route
9 console.log("Request URL:", request.url);
10
11 // Return NextResponse to continue, or redirect/rewrite
12 return NextResponse.next();
13}
14
15// Matcher — specify which routes this middleware applies to
16export const config = {
17 matcher: [
18 // Match all routes except static files and images
19 "/((?!_next/static|_next/image|favicon.ico|public/).*)",
20 ],
21};
22
23// More specific matchers
24export const config = {
25 matcher: [
26 // Only run on /dashboard routes
27 "/dashboard/:path*",
28
29 // Run on everything except API routes
30 "/((?!api/).*)",
31
32 // Match specific paths
33 "/about/:path*",
34 "/blog/:path*",
35 ],
36};
37
38// Multiple middleware files — not supported
39// Only ONE middleware.ts file per project
40// Use matcher config to handle different paths in one file

info

Place middleware.ts at the project root (or in /src if using src directory). It runs on the Edge Runtime, so Node.js APIs like fs are not available.
URL Rewriting & Redirecting
rewriting.ts
TypeScript
1import { NextResponse, type NextRequest } from "next/server";
2
3export function middleware(request: NextRequest) {
4 const { pathname } = request.nextUrl;
5
6 // Rewrite — serve different content without changing the URL
7 if (pathname.startsWith("/blog")) {
8 // User sees /blog/my-post but Next.js serves /articles/my-post
9 return NextResponse.rewrite(new URL(pathname.replace("/blog", "/articles"), request.url));
10 }
11
12 // Redirect — send user to a different URL
13 if (pathname === "/old-page") {
14 return NextResponse.redirect(new URL("/new-page", request.url));
15 }
16
17 // Temporary redirect (307)
18 return NextResponse.redirect(new URL("/new-location", request.url), 307);
19
20 // Permanent redirect (308)
21 return NextResponse.redirect(new URL("/new-location", request.url), 308);
22
23 // Rewrite based on hostname (multi-tenant)
24 const hostname = request.headers.get("host") ?? "";
25 if (hostname.includes("admin.example.com")) {
26 return NextResponse.rewrite(new URL("/admin" + pathname, request.url));
27 }
28
29 return NextResponse.next();
30}
31
32// A/B testing via rewriting
33function getVariant(request: NextRequest): string {
34 const cookie = request.cookies.get("ab-variant");
35 if (cookie) return cookie.value;
36
37 // Random assignment
38 return Math.random() < 0.5 ? "control" : "variant";
39}
40
41export function middleware(request: NextRequest) {
42 if (request.nextUrl.pathname === "/homepage") {
43 const variant = getVariant(request);
44 const response = NextResponse.rewrite(
45 new URL(`/homepage-${variant}`, request.url)
46 );
47 response.cookies.set("ab-variant", variant, {
48 maxAge: 60 * 60 * 24 * 30, // 30 days
49 });
50 return response;
51 }
52
53 return NextResponse.next();
54}
Headers & Cookies
headers_cookies.ts
TypeScript
1import { NextResponse, type NextRequest } from "next/server";
2
3export function middleware(request: NextRequest) {
4 const response = NextResponse.next();
5
6 // Setting response headers
7 response.headers.set("X-Request-Id", crypto.randomUUID());
8 response.headers.set("X-Frame-Options", "DENY");
9 response.headers.set(
10 "Strict-Transport-Security",
11 "max-age=63072000; includeSubDomains; preload"
12 );
13
14 // Reading request headers
15 const userAgent = request.headers.get("user-agent");
16 const authorization = request.headers.get("authorization");
17 const acceptLanguage = request.headers.get("accept-language");
18
19 // Reading cookies
20 const token = request.cookies.get("session-token")?.value;
21 const preferences = request.cookies.get("user-prefs");
22
23 // Setting cookies on response
24 response.cookies.set("last-visited", request.nextUrl.pathname, {
25 path: "/",
26 maxAge: 60 * 60 * 24 * 365, // 1 year
27 httpOnly: true,
28 secure: true,
29 sameSite: "lax",
30 });
31
32 // Deleting cookies
33 response.cookies.delete("old-cookie");
34
35 // Reading search params
36 const searchParams = request.nextUrl.searchParams;
37 const page = searchParams.get("page") ?? "1";
38 const sort = searchParams.get("sort") ?? "newest";
39
40 return response;
41}
Authentication Middleware
auth.ts
TypeScript
1import { NextResponse, type NextRequest } from "next/server";
2import { jwtVerify } from "jose";
3
4const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
5
6const protectedRoutes = ["/dashboard", "/settings", "/admin"];
7const authRoutes = ["/login", "/register"];
8
9async function verifyToken(token: string) {
10 try {
11 const { payload } = await jwtVerify(token, JWT_SECRET);
12 return payload;
13 } catch {
14 return null;
15 }
16}
17
18export async function middleware(request: NextRequest) {
19 const { pathname } = request.nextUrl;
20 const token = request.cookies.get("session-token")?.value;
21
22 // Check if the route is protected
23 const isProtected = protectedRoutes.some((route) =>
24 pathname.startsWith(route)
25 );
26
27 // Check if the route is auth-only (login, register)
28 const isAuthRoute = authRoutes.some((route) =>
29 pathname.startsWith(route)
30 );
31
32 if (isProtected) {
33 if (!token) {
34 // No token — redirect to login with return URL
35 const loginUrl = new URL("/login", request.url);
36 loginUrl.searchParams.set("returnTo", pathname);
37 return NextResponse.redirect(loginUrl);
38 }
39
40 const payload = await verifyToken(token);
41 if (!payload) {
42 // Invalid token — clear cookie and redirect
43 const response = NextResponse.redirect(new URL("/login", request.url));
44 response.cookies.delete("session-token");
45 return response;
46 }
47
48 // Add user info to headers for Server Components
49 const response = NextResponse.next();
50 response.headers.set("x-user-id", payload.sub as string);
51 response.headers.set("x-user-role", payload.role as string);
52 return response;
53 }
54
55 // Redirect logged-in users away from auth routes
56 if (isAuthRoute && token) {
57 const payload = await verifyToken(token);
58 if (payload) {
59 return NextResponse.redirect(new URL("/dashboard", request.url));
60 }
61 }
62
63 return NextResponse.next();
64}
65
66export const config = {
67 matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*", "/login", "/register"],
68};

info

Pass authenticated user info via request headers (x-user-id, x-user-role) so Server Components can access it without re-verifying the JWT.
Geolocation & Locale
geolocation.ts
TypeScript
1import { NextResponse, type NextRequest } from "next/server";
2
3export function middleware(request: NextRequest) {
4 // Geolocation data (available on Vercel, Cloudflare, etc.)
5 const country = request.geo?.country ?? "US";
6 const city = request.geo?.city ?? "Unknown";
7 const latitude = request.geo?.latitude;
8 const longitude = request.geo?.longitude;
9
10 // Locale detection from Accept-Language header
11 const acceptLanguage = request.headers.get("accept-language");
12 const preferredLocale = acceptLanguage?.split(",")[0]?.split("-")[0] ?? "en";
13
14 // Check cookie for user-selected locale
15 const cookieLocale = request.cookies.get("locale")?.value;
16
17 // Priority: cookie > Accept-Language > default
18 const locale = cookieLocale ?? preferredLocale;
19
20 // Redirect based on locale (if not already localized)
21 if (!request.nextUrl.pathname.startsWith(`/${locale}`)) {
22 const localeUrl = new URL(`/${locale}${request.nextUrl.pathname}`, request.url);
23 const response = NextResponse.redirect(localeUrl);
24 response.cookies.set("locale", locale, { maxAge: 60 * 60 * 24 * 365 });
25 return response;
26 }
27
28 // Country-based redirects
29 if (country === "CN") {
30 // Redirect Chinese users to Chinese domain
31 return NextResponse.rewrite(new URL("/zh" + request.nextUrl.pathname, request.url));
32 }
33
34 // Add geo info to headers for Server Components
35 const response = NextResponse.next();
36 response.headers.set("x-country", country);
37 response.headers.set("x-city", city);
38 if (latitude) response.headers.set("x-latitude", String(latitude));
39 if (longitude) response.headers.set("x-longitude", String(longitude));
40 return response;
41}
Best Practices

1. Keep middleware lean — it runs on every matched request. Heavy computation here hurts latency.

2. Use the matcher config to limit which routes trigger middleware.

3. Middleware runs on Edge Runtime — use jose or next/jwt instead of jsonwebtoken for JWT verification.

4. Pass data to Server Components via headers — avoid re-verifying tokens in every component.

5. Use NextResponse.rewrite() for internal routing, NextResponse.redirect() for external URL changes.

6. There is only ONE middleware file per project — use matcher config to handle different paths.

$Blueprint — Engineering Documentation·Section ID: NEXT-MW·Revision: 1.0