Next.js middleware lets you run code before a request finishes and update the response. Alongside Edge Functions, it’s a powerful tool that enables developers to achieve enhanced functionality and performance.
Middleware was introduced in Next.js v12 and has been improved in consecutive versions. Starting with Next.js v13, middleware can be used to respond directly to requests without going through the route handler. This can improve performance and security. Middleware can also be used to work with Vercel Edge Functions.
Edge Functions allow you to run code at the network’s edge. So, in this post, we’ll learn how middleware works with Edge Functions and why it’s important to know. Now, let’s get started!
Middleware in Next.js is a piece of code that allows you to perform actions before a request is completed and modify the response accordingly. It bridges the incoming request and your application, providing flexibility and control over the request/response flow.
In Next.js v12, middleware was introduced to enhance the Next.js router based on user feedback. In the later versions, middleware gained more popularity and improved its DX as well. In version v15, middleware now supports the Node.js runtime as well.
With middleware, you can seamlessly integrate custom logic into the request/response pipeline, allowing you to tailor your application’s behavior to specific requirements. It lets you modify requests and responses, making it easier to enrich headers, implement URL redirects, or keep track of incoming and outgoing requests. It’s a versatile tool that can improve your web application’s performance, security, and functionality.
Editor’s note: This post was updated by Abhinav Anshul in May 2025 to align with Next.js v15, and include practical use cases for both middleware and Edge Functions.
Let’s create a middleware and add a piece of code to show how it works. To use middleware, you need to create a file called middleware.js
or middleware.ts
at the same level as your app
directory. Unlike Next 12, where we have to add an underscore, Next v15 doesn’t require that. This file will contain the code you want to run before a request is completed.
Inside the file, paste this:
// middleware.ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(req: NextRequest) { const { pathname } = req.nextUrl; // Restrict the /api/hello endpoint to only POST if (pathname === "/api/hello") { if (req.method !== "POST") { return new NextResponse( `Cannot access this endpoint with ${req.method}`, { status: 400 } ); } } return NextResponse.next(); }
Middleware functions are crucial in processing requests and responses in a web application. Middleware works by intercepting requests and responses before they are passed to the application’s core code.
One of the major use cases of middleware is authentication. It provides a way to verify the user’s identity before granting them access to the application. By authenticating users, middleware ensures that only authorized individuals can access protected resources, enhancing the application’s overall security.
Authorization is another key feature of middleware. Once users are authenticated, middleware can implement access control based on their roles or permissions. This means certain pages or resources within the application can be restricted to specific users or user groups.
Additionally, middleware can use caching mechanisms to optimize the application’s performance. By storing frequently accessed data in a cache, middleware reduces the need to fetch it from the underlying database repeatedly.
However, unlike API routes, middleware does not return HTML, UI rendering, or any kind of JSON structure. Instead, it only processes a request through the application. This is by design and not exactly a limitation.
Here are the actions middleware can actually take:
NextResponse.next()
You can add a check or a condition before allowing middleware to reach to its usual destination. This check could be ensuring you have a user token, data, or any logical condition that you might wanna stop the middleware normal flow if that condition is not met.
For example, here I am ensuring the user is valid. Only the NextResponse.next()
would be called, and thus middleware continues:
if (isValidUser) { return NextResponse.next(); }
NextResponse.redirect(url)
This is essentially used for sending the user to a different URL
based on certain conditions. Here, if the user is not logged in, then this middleware would redirect the user to a login page. Now, you can also achieve this by adding a client-side logic.
But this approach brings security concerns, and you will see a flash of the page before the redirection. So middleware suits here the best:
if (!isLoggedIn) { return NextResponse.redirect(new URL("/login", request.url)); }
NextResponse.rewrite(url)
This will rewrite the request path without changing the URL in the browser. It can be useful for testing purposes or even serving content from a different server/path.
For example, here you are checking the locale
for the language. If it’s German (de
), then it will append the de
and serve the content in German:
if (locale === "de") { return NextResponse.rewrite(new URL("/fr" + request.nextUrl.pathname, request.url)); }
NextResponse(body, options)
Again, this is a constructor that will stop the request from proceeding further. Here you can send HTML, JSON, or plain text — but strictly no server-rendered content:
if (isRateExceeded) { return new NextResponse("Too many request, Please try after some time", { status: 429 }); }
Middleware allows you to add functionality to your application without modifying the application code. This makes it a great way to add new features or fix bugs without deploying a new version of your application. Additionally, middleware can be used to scale your application by offloading tasks to other servers.
While there are several advantages to using middleware, there are also downsides. Middleware can add complexity to your application. This can make developing, deploying, and maintaining your application more difficult.
Middleware can also add costs to your application. This is because you will need to purchase and maintain the cost of hosting the middleware on a server. In case of Next.js, using middleware means you are locked to the Next.js environment. If you have to move out of Next.js and create JS bundles and host them somewhere, it would be a bottleneck.
Lastly, middleware causes latency in your application. This is because middleware code needs to be executed before your application can process the request. To mitigate this, you might need to handle it with loaders or skeleton screens that might be substandard for a user experience.
Here are a few scenarios where Next.js middleware can be used:
Next.js middleware provides access to geographic information through the NextRequest
API’s geo
key. This allows you to serve location-specific content to users. For instance, if you’re building a website for a shoe company with branches in different regions, you can display trending shoes or exclusive offers based on the user’s location.
Using the cookies
key available in the NextRequest
API, you can set cookies and authenticate users on your site. You can also enhance security by blocking bots, users, or specific regions from accessing your site. By rewriting the request, you can redirect them to a designated blocked page or display a 404 error.
A/B testing involves serving different versions of a site to visitors to determine which version elicits the best response. Previously, developers performed A/B testing on the client side on static sites, resulting in slower processing and potential layout shifts.
However, Next.js middleware enables server-side processing of user requests, making the process faster and eliminating layout shifts. A/B testing with middleware involves using cookies to assign users to specific buckets, and servers then redirect users to either the A or B version based on their assigned bucket.
You can use middleware to prevent abuses by implementing a rate limiter. Next.js middleware allows you to implement a lightweight basic rate limiter without the need for any traditional backend server or databases.
It can also be highly customizable based on IP, headers, etc. Underneath the hood, it tracks the request coming from a particular IP address. If the requests exceed a certain defined threshold, the middleware will block the upcoming requests.
The code above checks if the request URL matches the /api/hello
pattern. If it does, the code checks if the request method is a POST
method. If it is not, the code returns a 400 Bad Request
response with an error message that includes the request method.
If the request method is POST
, it calls the NextResponse.next()
function to tell Next.js to continue processing the request. In the case below, the API is being hit on the browser, hence likely to be a GET
request, therefore you will get this on your browser:
Now that you have an understanding of middleware, it’s only fair to talk about Edge Functions, which are deeply interwoven with middleware concepts. Let’s have a quick refresher on Edge functions and how they help.
If you’ve ever used serverless functions, you’ll understand the importance of Edge Functions. To better understand, we’ll compare Edge Functions to serverless functions.
When you deploy a serverless function to Vercel, it’s deployed to a server somewhere in the world. The request made to that function will then execute where the server is located.
If a request is made to a server close to that location, it will happen quickly. But if a request is made to the server from a faraway location, the response will be much slower. This is where Edge Functions can help. Essentially, Edge Functions are serverless functions that run geographically close to a user, making the request very fast regardless of where that user might be.
When deploying a Next.js application to Vercel, the middleware will deploy as Edge Functions to all regions worldwide. This means that instead of a function sitting on a server, it will sit on multiple servers. Here, the Edge Functions use middleware.
One of the unique things about Edge Functions is that they are much smaller than our typical serverless functions. They also run on V8 runtime, which makes them 100x faster than Node.js in containers or virtual machines.
Let’s create a new API route, although Next.js comes with a default one, which is hello.js
. To do this, we’ll add a file directly in our pages/api
sub-folder of our project. Let’s paste this in:
import { NextResponse } from 'next/server'; export const config = { runtime: 'edge', //This specifies the runtime environment that the middleware function will be executed in. }; export default (request) => { return NextResponse.json({ name: `Hello, from ${request.url} I'm now an Edge Function!`, }); };
You can then deploy to Vercel or any edge computing platform of your choice.
Understanding how to use middleware with Edge Functions efficiently solves the issue of sharing a common login across multiple applications (commonly seen with authentication, bot protection, redirects, browser support, feature flags, A/B testing, server-side analytics, logging, and geolocations).
With the help of Edge Functions, middleware can run faster, like static web applications, because they help to reduce latency and eliminate cold startup.
With Edge Functions, we can run our files on multiple geolocations, allowing regions closest to the user to respond to the user’s request. This provides faster user requests regardless of their geographic location.
Traditionally, web content is served from a CDN to an end user to increase speed. However, because these are static pages, we lose dynamic content. Also, we use server-side rendering to get dynamic content from the server, but we lose speed.
However, deploying our middleware to the Edge like a CDN brings our server logic closer to our visitors’ origin. As a result, we have speed and personalization for the users. As a developer, you can now build and deploy your website and then cache the result in CDNs across the world.
Let’s see how we can use middleware and Edge Functions. We’ll use the middleware to create a protected endpoint for the Edge Functions. First, open up the terminal and create a folder where we want our project installed:
mkdir next_middleware
Then, cd
into the recently created folder, and install Next.js using this command:
npx create-next-app@latest
Give the project a name and choose other preferences during the installation process. Make sure you are opting for the app router for the latest Next.js features.
Now that we’ve done that, let’s create our middleware. Let’s create a file called middleware.js
at the root as our app
directory. Next, let’s paste in the following:
// middleware.js import { NextResponse } from "next/server"; import { NextRequest } from "next/server"; export function middleware(request) { // Get the admin cookie id from the request. let adminCookieId = request.cookies.get("adminCookieId")?.value; // If the admin cookie id is not "abcdefg", redirect the user to the admin login page. if (adminCookieId !== "abcdefg") { return NextResponse.redirect(new URL("/admin-login", request.url)); } } export const config = { matcher: "/api/protected/:path*",//The matcher specifies the URL patterns that this middleware will be applied to. };
In the code above, middleware.js
defines a middleware function that can be used to protect a Next.js API endpoint. The middleware function checks the request for an adminCookieId
. If the adminCookieId
is not present or is not equal to "abcdefg"
, the middleware function redirects the user to the admin login page.
Let’s create our protected endpoint. Inside our pages/api
folder, create a protected
folder. Then, create an index.js
folder inside it and paste this:
// do this only if you are using the pages folder, this has been deprecated in the app router //export const config = { // runtime: "edge", // }; export default function handler(req, res) { const { language } = req.query; // Personalization logic based on user preferences let greeting; if (language === "en") { greeting = "Hello! Welcome!"; } else if (language === "fr") { greeting = "Bonjour! Bienvenue!"; } else if (language === "es") { greeting = "¡Hola! ¡Bienvenido!"; } else { greeting = "Welcome!"; } res.status(200).json({ greeting }); }
In case you’re using the app
router, you can move the logic to this folder
app/api/protected/route.js
:
export const runtime = 'edge'; // Run this API route at the edge using edge function export async function GET(request) { // getting the slug using searchParam as app router is a server rendered component const { searchParams } = new URL(request.url); const language = searchParams.get('language'); // Personalization logic based on query parameter let greeting; switch (language) { case 'en': greeting = 'Hello! Welcome!'; break; case 'fr': greeting = 'Bonjour! Bienvenue!'; break; case 'es': greeting = '¡Hola! ¡Bienvenido!'; break; default: greeting = 'Welcome!'; } return new Response(JSON.stringify({ greeting }), { status: 200, headers: { 'Content-Type': 'application/json', }, }); }
The code above defines an Edge Function that can be used to personalize greetings for users based on their language preferences. The function gets the language from the request query parameters and then uses it to select the appropriate greeting. The function then returns the greeting as a JSON response.
The config
object specifies that the function will be executed at the edge. This means that the function will be executed close to the user, which can improve performance. Here’s our final result:
In this article, we discussed what middleware is, how it works, and its advantages and disadvantages. We also went into depth to understand its implications and shortcomings as well.
We talked briefly about Edge Functions, their use cases, and how to create a simple edge function ourselves. When talking about middleware, Edge Functions play a key role in deployment and creation of the user experience.
With its ability to solve basic problems like authentication and geolocation, middleware is an outstanding feature. With the help of Edge Functions, middleware can run much faster, like static web applications.
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket captures console logs, errors, network requests, and pixel-perfect DOM recordings from user sessions and lets you replay them as users saw it, eliminating guesswork around why bugs happen — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe Google Gemini AI model has integrated AI-powered features to improve the debugging experience in web development.
The Node.js 24 release included significant updates. Explore 10 features you might not be using yet — but absolutely should be.
Explore six of the best, easiest, most configurable, and convenient CSS animation libraries available in 2025.
A critical auth bypass vulnerability in Next.js lets attackers skip middleware checks by faking the x-middleware-subrequest
header.
3 Replies to "A crash course in Next.js middleware"
Thank you for the article.
Would this mean that using Middleware in Next.js disables the static page generation and as a consequence every page becomes server side rendered?
No, the middleware doesn’t disable the static pages but works with it by modifying or manipulating the pages by sending responses depending on the req(cookies,headers,geolocation) parameters.
Thanks but is hardcoding a username and password a valid production case? I would expect in a real world to have the users and password stored in a database, which is not at the edge, limiting then the authentication use case. I’m struggling to understand this use case. Do people really do that?