Overview

A middleware.ts at the project root runs before the matched request reaches a route. Use it for cross-cutting work that has to happen on every request: auth gating, locale routing, A/B redirects, header injection. The middleware runs on the Edge runtime by default; the constraints are real, and the cost compounds because the function fires on every matched request.

Place middleware.ts at the project root

The file lives at the project root, not inside app/ or pages/. There is one middleware per project. It exports a default function (or a named middleware function) that accepts a NextRequest and returns a NextResponse.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(req: NextRequest) {
  return NextResponse.next();
}

A return value of NextResponse.next() lets the request proceed. Any other response short-circuits the pipeline; the route handler never runs.

Constrain the matcher; do not run on every asset

The default matcher runs on every request, including static assets. That is wasteful and expensive. Constrain it.

export const config = {
  matcher: [
    // Skip Next internals and static files; match everything else.
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)).*)",
  ],
};

For specific routes, list them: matcher: ["/dashboard/:path*", "/admin/:path*"]. The matcher is the cheapest filter; pruning here pays for itself on every page load.

Pick redirect, rewrite, or next deliberately

Three primitives cover most middleware logic:

  • NextResponse.redirect(url, status): tell the browser to navigate elsewhere. The URL changes; the user sees a 30x.
  • NextResponse.rewrite(url): serve a different route under the original URL. The browser does not see the rewrite; the user keeps the URL they typed.
  • NextResponse.next(): continue to the matched route, optionally with mutated request headers.
export function middleware(req: NextRequest) {
  const country = req.geo?.country ?? "US";
  if (req.nextUrl.pathname === "/") {
    return NextResponse.rewrite(new URL(`/landing/${country.toLowerCase()}`, req.url));
  }
  return NextResponse.next();
}

Use rewrites for A/B routing and locale serving. Use redirects for canonical URL enforcement and moved pages. Use next when you only need to mutate headers or cookies.

Read and write cookies on req and the response

Cookies live on req.cookies for reading and on the response for writing. Set a cookie in the response that goes back to the browser.

const res = NextResponse.next();
const sid = req.cookies.get("sid")?.value;
if (!sid) {
  res.cookies.set("sid", crypto.randomUUID(), {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
}
return res;

Always set httpOnly, secure, and an explicit sameSite on auth cookies. The middleware is the right place to rotate session tokens, because it runs before the page reads them.

Forward signal via request headers, not globals

To pass data from middleware to a page or handler, set request headers on the response and read them server-side in the route.

const res = NextResponse.next({
  request: { headers: new Headers({ ...req.headers, "x-user-id": userId }) },
});
return res;

In the route, read with headers() from next/headers. Do not try to share state in module scope; the Edge runtime isolates each invocation.

Run on Edge by default; switch to Node only when forced

Middleware runs on the Edge runtime: V8 isolates, Web APIs only, no node:* modules, no native dependencies, no filesystem. Cold starts are sub-50ms, execution is global. The constraints come from the runtime, not the framework.

Switch to the Node runtime per project if you must:

export const config = { runtime: "nodejs" };

But the right answer is usually to move Node-only work to a server component, a route handler, or a server action. Middleware that needs a heavy SDK or a database connection probably belongs elsewhere.

Treat middleware as a hot path

Middleware runs on every matched request. A 50ms middleware function adds 50ms to every page in the matcher. Budget accordingly.

  • Cache token-verification results in a cookie or a fast KV. Do not call the auth provider on every request.
  • Skip JSON parsing of large request bodies; reroute the work to the route handler.
  • Avoid calling external APIs unless the request truly needs the call.
  • Profile cold and warm invocations on the host (Vercel surfaces this; see vercel).

For per-request work that is not cross-cutting, move it to the route. For cross-cutting work that is too expensive for Edge, push it to the route and let middleware do only the matcher and the redirect.