Next.js middleware is the gate that runs after a request hits your app and before a route decides what HTML or JSON to send. That is why teams keep looking here for redirects, auth cookies, and locale — the fix lives one layer earlier than the page.

If you only put logic inside a Server Component or a useEffect, the browser already started loading the wrong URL. Middleware exists so you can change the path, set a header, or bounce unauthenticated traffic before that work begins.

What Next.js Middleware Is For

Think of it as a short checklist at the door, not the whole office behind the desk.

  • Redirects: old marketing URLs, trailing slashes, www vs apex, or a /login bounce when a session cookie is missing.
  • Headers: security headers, a request ID, or a flag your layout reads later.
  • Light gates: “this path needs a cookie” or “this path is US-only,” not a full database lookup on every hit.

It is a poor place for slow I/O, large libraries, or business rules that belong in a Server Action or an API route. Keep the file small — this code runs on every matched request.

On Next.js 15 and earlier, that file is middleware.ts at the project root (or under src/). On Next.js 16+, the same job moved to the proxy.ts convention, and the exported function is named proxy — same idea, new name. The rest of this guide uses the word middleware for the pattern; rename the file and export if your major version expects proxy.ts.

Dark circular Next.js logo with a white N floating in purple starry space

A Minimal Next.js Middleware Example

A common first win is a redirect that does not wait for React.

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

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  if (pathname === "/old-pricing") {
    return NextResponse.redirect(new URL("/website-pricing", request.url));
  }

  const response = NextResponse.next();
  response.headers.set("x-pathname", pathname);
  return response;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

The matcher matters. Without it, middleware can run on every static asset and burn budget for no reason. Scope it to the paths you actually care about — dashboard prefixes, /app/*, or a short list of legacy URLs.

For auth-shaped gates (cookie present → continue, missing → /login), use the same pattern, then harden the session check on the server. Our Next.js authentication and protected routes guide covers Auth.js v5 and why middleware alone is not a full security model.

What Belongs Elsewhere

Middleware should answer “should this request continue, and with which headers?” It should not answer “what is this user’s invoice total?”

JobBetter home
Form submit / mutationsServer Actions or route handlers
Personalized product dataServer Components + your data layer
Heavy auth provider logicAuth library + server session helpers
Dynamic product URLsApp Router dynamic routes

If you need streaming UI or Suspense boundaries, that is layout and page work — see Next.js streaming layouts. Middleware does not replace those tools; it only decides whether the request reaches them.

When Middleware Saves Real Product Time

Teams usually add it after a painful moment. A public URL that should have died last quarter still ranks. A client hits /dashboard logged out and sees a flash of private UI. Or a regional launch needs a soft geo redirect before marketing builds ten more pages.

Those are product bugs as much as code bugs. A two-file middleware fix is cheaper than teaching every page to remember the rule. When the app grows past a brochure site into logged-in workflows, you are in web app territory — middleware is one of the seams that keeps that product from leaking the wrong screen.

If you are wiring redirects and session gates into a Next.js product and want a second pair of eyes on the architecture, book fifteen minutes or start from contact. Bring the routes that must stay private and the URLs you still need to honor.