Next.js Middleware lets you run Node.js-like code before a request is completed, right at the edge of your network, before it even hits your Next.js application.

Here’s a Next.js middleware function in action, intercepting a request and rewriting the URL:

// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  const url = request.nextUrl.clone()
  url.pathname = `/about${url.pathname}` // Prepend /about to the path
  return NextResponse.rewrite(url)
}

export const config = {
  matcher: '/:path*', // Match all paths
}

If you visit /users/123, this middleware rewrites the URL to /about/users/123 before Next.js tries to find a page for /users/123. Your pages/about/users/[id].js (or app/about/users/[id]/page.js) would then handle the request. This is powerful for A/B testing, authentication, and dynamic routing without touching your core page components.

The core problem Next.js Middleware solves is enabling fine-grained request manipulation and conditional logic at the earliest possible point in the request lifecycle. Traditionally, this kind of logic would live in a separate proxy server, an API gateway, or within each individual page/route handler. Middleware centralizes this, making it more maintainable and performant. It leverages the Web Request and Fetch API standards, meaning the code you write feels familiar if you’ve worked with modern JavaScript APIs.

Internally, when a request hits your Next.js application, the middleware function executes. It receives a Request object and can return one of several NextResponse types:

  • NextResponse.next(): Continues to the next middleware or the matched route.
  • NextResponse.rewrite(url): Internally rewrites the URL to a different path without changing the URL in the browser’s address bar. The user never sees the rewritten URL.
  • NextResponse.redirect(url): Sends a 307 or 308 redirect to the browser, changing the URL in the address bar.
  • NextResponse.json(body): Responds directly with JSON.
  • NextResponse.text(body): Responds directly with text.
  • NextResponse.file(filePath): Responds with a file.

The matcher configuration in middleware.js is crucial. It uses glob patterns to define which paths the middleware should run on. Without it, the middleware would run on every request, including static assets, potentially slowing things down. For example, matcher: ['/admin/:path*', '/api/:path*'] would only execute the middleware for requests starting with /admin/ or /api/.

The most surprising thing about middleware is how easily it can be used to implement dynamic routing patterns that would otherwise require complex server-side logic or multiple build steps. For instance, you can create a single middleware that handles internationalization by inspecting the Accept-Language header or a subdomain, and then rewrites the URL to the appropriate language-specific route (/en/products, /fr/produits) without needing separate deployment targets for each language.

When you configure middleware to rewrite a request, it’s not a client-side redirect or a server-side redirect. The browser’s URL bar remains unchanged, and the server still serves content for the original URL. The NextResponse.rewrite() method essentially tells the Next.js server, "Okay, I’ve decided what the user actually wants to see, and it’s this other path. Please go fetch that content and serve it as if the user had requested it directly, but keep the original URL in the address bar." This is distinct from NextResponse.redirect(), which explicitly tells the browser to make a new request to a different URL.

The next concept you’ll likely explore is how to manage cookies and headers within middleware to maintain state or pass information between requests.

Want structured learning?

Take the full Nextjs course →