Skip to content

Next.js Middleware

Middleware runs before a request completes, letting you inspect, redirect, rewrite, or modify it across many routes from a single file. This lesson covers writing middleware and its most common use cases.

One File, Runs Before Every Matching Request

A single middleware.ts file at your project's root (next to app/, or inside src/ if you use that convention) can intercept requests before they reach any route. It runs on the Edge Runtime by default, which is lightweight and fast but has a smaller API surface than the standard Node.js runtime.

Middleware is commonly used for cross-cutting concerns that apply to many routes at once — checking authentication before allowing access to a section of the site, redirecting based on geolocation or A/B test buckets, or rewriting requests for internationalization.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const isLoggedIn = request.cookies.has("session");

  if (!isLoggedIn && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

This middleware redirects unauthenticated visitors away from any /dashboard/* route, before the actual page ever renders.

Middleware File and Config Shape

export function middleware(request: NextRequest) {
  // inspect request.nextUrl, request.cookies, request.headers
  return NextResponse.next(); // continue as normal
}

export const config = {
  matcher: ["/pattern/:path*"], // limits which paths run this middleware
};
  • The file must be named middleware.ts (or .js) and live at the project root.
  • NextResponse.next() lets the request continue to its normal destination unchanged.
  • NextResponse.redirect(url) and NextResponse.rewrite(url) change where the request goes.
  • The matcher config limits which paths trigger the middleware, improving performance.

Middleware Cheat Sheet

Common actions available inside middleware.

Action Code
Continue normally NextResponse.next()
Redirect NextResponse.redirect(new URL("/login", request.url))
Rewrite (keep URL, serve different content) NextResponse.rewrite(url)
Read a cookie request.cookies.get("name")
Set a response header response.headers.set("X-Custom", "value")

Scoping Middleware with matcher

Without a matcher, middleware runs on every request, including static assets, which is usually wasteful. The matcher config accepts path patterns (similar to dynamic route syntax) to scope middleware to exactly the routes that need it.

export const config = {
  matcher: [
    "/dashboard/:path*",
    "/account/:path*",
  ],
};

This middleware now only runs for paths under /dashboard or /account.

Edge Runtime Limitations

Because middleware runs on the Edge Runtime by default, it does not have access to full Node.js APIs — you can't use most database drivers or Node-specific modules directly inside middleware.ts. For heavier logic, middleware should make lightweight checks (like reading a cookie) and delegate real work to a Route Handler or Server Component.

Common Mistakes

  • Forgetting to return NextResponse.next() at the end, accidentally blocking requests that should pass through.
  • Omitting a matcher, causing middleware to run on every single request, including static files.
  • Trying to use Node.js-only libraries (like a full SQL driver) directly inside middleware.
  • Doing expensive work (like a slow API call) inside middleware, adding latency to every matched request.

Key Takeaways

  • middleware.ts runs before matching requests reach their route, on the Edge Runtime by default.
  • It can redirect, rewrite, or pass a request through unchanged via NextResponse.
  • matcher scopes which paths trigger the middleware, avoiding unnecessary overhead.
  • Common use cases include auth checks, geolocation redirects, and internationalization rewrites.
  • Middleware has a limited runtime — keep its logic lightweight and delegate heavier work elsewhere.

Pro Tip

Use middleware for fast, cheap decisions ("is there a session cookie at all?"), and verify anything security-critical ("is this session actually valid?") again inside the protected route itself — middleware is a first line of defense for UX redirects, not a substitute for real authorization checks in your data layer.