Middleware in Next.js 12: What It Does and Where It Fits
Next.js 12 introduced middleware as a beta feature. In broad computing terms, middleware is the "glue" between two systems. Here, it sits between a user's request and the response, letting you intercept that request and modify the response via redirects, rewrites, header changes, or even streaming HTML.
Middleware runs in a limited "Edge Runtime" rather than the full Node.js environment. It has access to standard Web APIs. On Vercel, these functions deploy as Edge Functions.
Middleware vs. API Routes
The obvious comparison is to API routes, which have been in Next.js for longer. The fundamental difference is placement: API routes are traditionally hosted on a single Node server, while middleware functions are designed for the "edge"—code deployed in multiple locations globally. That geographic spread shortens the physical distance between user and server, and edge deployments typically pair with aggressive caching and efficient cache invalidation. The result is faster responses for a broader set of users.
A second key difference is boot time. API routes deployed as serverless functions can take around 250ms to start up, which contributes to slow responses. Middleware is built to avoid meaningful cold starts entirely. Vercel claims Edge Functions start up roughly 100x faster than its Serverless Functions.
Functionally, middleware can be scoped to a directory and applied to multiple pages, which lets you avoid repeating logic. For example, you could place a middleware file in an app directory to read user cookies, determine login state, and pass that data to every page underneath. Recreating that pattern with API routes would require extra work per route.
Use Cases for Middleware
Middleware is appropriate when a task requires only a small amount of processing—it must return a response within 1.5 seconds or the request times out.
Geolocation
The NextRequest object exposes geographic data under the geo key. You can use that to rewrite a user to a localized page—for example, a multinational restaurant chain serving different menus by region. Vercel's examples include power parity pricing and i18n integration.
Security
Cookies are available on the cookies key of NextRequest, and NextResponse can set them. That allows cookie-based authentication. You can also restrict access by returning a 404 or rewriting to a "blocked" page for specific users—for instance, blocking by country. Vercel's geolocation block example demonstrates this.
A/B Testing
On a static site, client-side A/B testing often causes cumulative layout shift or a flash of the wrong content. Moving the decision to the server avoids that. You can assign visitors to "buckets" via cookies and redirect them accordingly. Vercel's simple A/B testing example shows the pattern.
Limitations to Know
Middleware has constraints that keep API routes relevant for certain workloads.
Execution and Size Limits (Vercel)
A middleware function may run for a maximum of thirty seconds, but it must return a response within the first 1.5 seconds. Any longer-running work, like logging to a database, should happen after the response is sent. Additionally, the total bundle containing the middleware function must stay under 1MB.
No Node.js APIs
Middleware does not run through Node.js like API routes do. Reading and writing to the filesystem is unavailable, and any JavaScript module that depends on native Node.js APIs cannot be used.
ES Modules Only
While you can use Node modules in middleware, they must be ES Modules. CommonJS packages—or packages that transitively rely on CommonJS—won't work.
No Dynamic Code Evaluation
Neither JavaScript's eval nor new Function(evalString) is permitted inside the runtime.
Building a Link Shortener with Middleware
To see middleware in action, we can build a URL shortener—a pattern that would normally rely on API routes. Start by cloning the starter app:
yarn create next-app -e https://github.com/sampoder/middleware-demo/tree/starter
The starter includes two key files: routes.js, which holds a hardcoded key/value map of short links (a stand-in for a database), and pages/index.js, which lists all available routes.
Middleware files live in the pages directory and are named _middleware.js. Scoping is directory-based: a file in /pages affects routes like /about and /about/team/john, while one in /pages/blog would affect /blog/middleware but not /info.
Create the new file and import NextResponse from next/server:
import { NextResponse } from 'next/server'
NextResponse extends the standard Response interface, giving us the ability to alter the response. Next, import the routes file:
import routes from "../routes"
Each middleware file must export a function named middleware. Next.js invokes it on every matching request:
export function middleware(req) {
}
That function receives a request object, an extension of the standard Request interface. The current path is accessible via the nextUrl key:
let { pathname } = req.nextUrl;
For the shortener, we check whether the routes object contains the pathname as a key:
if (routes[pathname]) {
}
If it matches, we use NextResponse.redirect() to send the user to the destination:
if (routes[pathname]) {
return NextResponse.redirect(routes[req.nextUrl.pathname])
}
When no destination exists for the pathname, we redirect to the homepage. Note that we cannot redirect directly to /, because relative URL support in middleware is slated for deprecation. Instead, we clone the request URL, modify its pathname, and pass that URL object to redirect():
else{
const url = request.nextUrl.clone()
url.pathname = '/'
return NextResponse.redirect(url)
}
The complete middleware function looks like this:
import { NextResponse } from "next/server";
import routes from "../routes";
export function middleware(req) {
let { pathname } = req.nextUrl
if (routes[pathname]) {
return NextResponse.redirect(routes[req.nextUrl.pathname])
}
else{
const url = request.nextUrl.clone()
url.pathname = '/'
return NextResponse.redirect(url)
}
}
The full codebase is available at https://github.com/sampoder/middleware-demo.
This example is intentionally simple, but it shows the core middleware pattern: intercept a request, apply minimal logic, and return a fast response. The feature still has room to grow, but even in its current beta state it opens up server-side processing that previously required more heavyweight infrastructure.



