Middleware runs before a request reaches a page or route handler. It runs in the Edge runtime, close to the user. Use it for:
1// middleware.ts (project root, sibling of app/)
2import { NextResponse, type NextRequest } from "next/server";
3
4export function middleware(req: NextRequest) {
5 // 1. Inspect the request
6 const isApi = req.nextUrl.pathname.startsWith("/api");
7
8 // 2. Decide what to do
9 if (isApi && !req.headers.get("x-api-key")) {
10 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 }
12
13 // 3. Pass through (default)
14 return NextResponse.next();
15}
16
17export const config = {
18 matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
19};matcher decides which paths run through middleware. Anything outside the matcher skips it.
1export const config = {
2 matcher: [
3 "/dashboard/:path*",
4 "/account/:path*",
5 "/api/:path*",
6 ],
7};Be generous in what to exclude — never run middleware on _next/static, which would 10× your edge invocations.
1return NextResponse.next(); // continue
2return NextResponse.redirect(new URL("/login", req.url)); // 302
3return NextResponse.rewrite(new URL("/eu", req.url)); // URL unchanged, served from /eu
4return new NextResponse("Forbidden", { status: 403 }); // short-circuit1const token = req.cookies.get("session")?.value;Middleware can read and write cookies:
1const res = NextResponse.next();
2res.cookies.set("visit", String(Date.now()));
3return res;Add a header in middleware → pages read it via headers():
1const res = NextResponse.next();
2res.headers.set("x-pathname", req.nextUrl.pathname);
3return res;1import { headers } from "next/headers";
2const pathname = (await headers()).get("x-pathname");Useful for layouts that need to know the current pathname.
1import { NextResponse, type NextRequest } from "next/server";
2import { jwtVerify } from "jose";
3
4const PUBLIC = ["/", "/login", "/signup", "/blog"];
5const isPublic = (p: string) => PUBLIC.some((x) => p === x || p.startsWith(`${x}/`));
6
7export async function middleware(req: NextRequest) {
8 const { pathname } = req.nextUrl;
9
10 if (isPublic(pathname)) return NextResponse.next();
11
12 const token = req.cookies.get("session")?.value;
13 if (!token) {
14 const url = req.nextUrl.clone();
15 url.pathname = "/login";
16 url.searchParams.set("next", pathname);
17 return NextResponse.redirect(url);
18 }
19
20 try {
21 await jwtVerify(token, new TextEncoder().encode(process.env.SESSION_SECRET!));
22 return NextResponse.next();
23 } catch {
24 const url = req.nextUrl.clone();
25 url.pathname = "/login";
26 return NextResponse.redirect(url);
27 }
28}
29
30export const config = {
31 matcher: ["/((?!api|_next|favicon.ico).*)"],
32};fs, crypto's sync APIs, native modules)cookies() from next/headers (use req.cookies instead)