Next.js

Next.js Middleware👨‍💻

Middleware is the code that runs before every matched request hits your route. In Next.js, you define it in a single file at the project root (middleware.ts or, in Next.js 16, proxy.ts) and it intercepts requests at the network edge. You get access to cookies, headers, the URL, and geolocation data. You can redirect, rewrite, set headers, or return a response directly -- all before your page or API route renders a single byte.

The mental model is straightforward: middleware is a function that receives a NextRequest and must return a NextResponse. Every millisecond you spend here is added to every matched request, so the rule is simple -- keep it fast, keep it thin. Auth checks, locale detection, feature flags, CORS headers. Not database queries, not heavy computation.

Key Takeaways

  • 1Middleware runs before route resolution on every matched request -- place `middleware.ts` (or `proxy.ts` in Next.js 16) at the project root, next to the `app/` directory
  • 2Use the `config.matcher` export to limit which paths trigger middleware. Without it, middleware runs on every request including static assets, which kills performance
  • 3NextResponse provides four core actions: `next()` to continue, `redirect()` to change the browser URL, `rewrite()` to serve different content at the same URL, and `json()` to return data directly
  • 4Read cookies with `request.cookies.get('name')?.value` and headers with `request.headers.get('name')` -- use these for auth checks, locale detection, and feature flags
  • 5Middleware runs in a limited runtime (Edge by default in earlier versions, Node.js in Next.js 16). Avoid heavy dependencies, database connections, or anything that adds latency to every request
  • 6Middleware is not a security boundary on its own -- always validate authentication again in your Server Components and Server Actions, close to where data is accessed

Master next.js middleware

Take the Next.js Full-Stack course with hands-on lessons and challenges.

Examples

Auth redirect -- the pattern every production app uses

typescript

Check for a session cookie on protected routes. If missing, redirect to login with a `from` param so the login page can bounce the user back after authentication. The matcher ensures this only runs on routes that actually need protection -- not on images, fonts, or public pages.

Locale detection and URL rewriting

typescript

Detects the user's preferred locale from a cookie or the Accept-Language header, then rewrites the URL so the correct localized content is served. The browser URL stays clean (/about instead of /en/about). The negative lookahead matcher excludes API routes and static assets.

A/B testing with cookie-based assignment

typescript

Assigns users to an A/B test variant via cookie. First-time visitors get randomly bucketed; returning visitors see the same variant. The rewrite is invisible -- the user always sees /pricing in their browser while the server renders the variant-specific page.

CORS headers for API routes

typescript

Handles CORS at the middleware layer so every API route gets consistent headers without duplicating logic. Preflight (OPTIONS) requests get a 204 with the right headers. Actual requests get the Allow-Origin header attached. Only whitelisted origins are permitted.

Multi-tenant URL rewriting

typescript

Extracts the subdomain from the Host header and rewrites the URL to a tenant-specific folder. The user sees acme.myapp.com/pricing but the server renders the page at /tenants/acme/pricing. This is the foundation of multi-tenant SaaS routing without deploying separate apps per customer.

Request logging and security headers

typescript

Adds security headers and a unique request ID to every response. The request ID flows through to your logging infrastructure, making it possible to trace a single user request across server components, API routes, and external services. Security headers are set once in middleware instead of repeated in every route.

Common Mistakes

Mistake:

Not setting a matcher -- middleware runs on every request including images, fonts, CSS, and internal Next.js routes, adding latency to everything

Fix:

Always export a `config.matcher`. At minimum, exclude static assets with the negative lookahead pattern: `'/((?!_next/static|_next/image|favicon.ico).*)'`. Better yet, list only the specific paths that need middleware.

Mistake:

Making database calls or heavy async operations inside middleware, turning every page load into a waterfall

Fix:

Middleware runs on every matched request. Verify JWTs with a lightweight library like `jose` (just a signature check). If you need to hit a database, do it in a Server Component or Route Handler where caching and streaming can absorb the latency.

Mistake:

Creating redirect loops by redirecting to a path that is itself matched by the middleware (e.g., protecting `/login` while redirecting unauthenticated users to `/login`)

Fix:

Either exclude redirect destinations from your matcher, or add an early return for public paths like `/login`, `/signup`, and `/forgot-password` before running any auth logic.

Mistake:

Treating middleware as the sole security layer -- assuming that because middleware blocks unauthenticated users, Server Components and API routes don't need their own checks

Fix:

Middleware is a UX optimization (fast redirects), not a security guarantee. Always re-validate authentication in Server Components and Server Actions, close to where data is accessed. Middleware can be bypassed through direct API calls or internal rewrites.

Best Practices

  • Keep middleware under 50ms. It runs on every matched request, so treat it like a hot path. No database queries, no external HTTP calls, no heavy computation. JWT signature verification with `jose` is fine; fetching a user profile from Postgres is not.
  • Use the most specific matcher possible. `'/dashboard/:path*'` is better than `'/(.*)'`. Overly broad matchers waste cycles on static assets and create subtle bugs when middleware accidentally processes internal Next.js routes.
  • Chain middleware logic with early returns. Check conditions in order of likelihood -- if 80% of requests are authenticated, check the token first and return `NextResponse.next()` fast. Put the redirect path (the minority case) after.
  • Pass data from middleware to downstream routes via request headers (`response.headers.set('x-user-id', ...)`) instead of trying to share state through globals or cookies set mid-request. Server Components can read these headers with the `headers()` function.
  • Test your matchers during development by logging `request.nextUrl.pathname` at the top of your middleware function. You will be surprised how many requests hit middleware when the matcher is misconfigured -- favicon, manifest, service worker, prefetch requests.

Summary

Next.js middleware (renamed to proxy in Next.js 16, though the file can still be `middleware.ts`) intercepts requests before they reach your routes. Use it for auth redirects, locale detection, A/B testing, CORS, multi-tenant rewrites, and security headers. The core API is `NextRequest` (cookies, headers, URL) in and `NextResponse` (next, redirect, rewrite, json) out. Always configure a matcher to avoid running on static assets, keep the function fast, and never rely on middleware as your only security layer -- validate auth again in Server Components and route handlers.

Practice Next.js with hands-on challenges

Learn next.js middleware hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Next.js with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.