Editor’s note: This guide to using Next.js’ middleware and Edge Functions was last updated on 14 June 2023 to reflect changes made with Next.js v13. This update also includes new sections that go further in-depth on middleware and Edge Functions and a section on creating Edge Functions. To get started with Next.js, check out our guide to the best Next.js starter templates.
Next.js is a React framework with several features that make building and deploying web applications easy. It offers powerful features such as middleware and Edge Functions, enabling developers to achieve enhanced functionality and performance.
Middleware was introduced in Next.js v12 and has been improved in Next.js v13. In Next.js v13, middleware can now 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!
Jump ahead:
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. Next.js v13 has further improved the middleware API to enhance the developer experience and add powerful functionality.
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 allows you to 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.
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 functions 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 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.
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.
This can boost performance while also reducing the load on your application servers. Middleware can be used to add security features to your application. This can help to protect your application from attacks and unauthorized access.
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 add cost to your application. This is because you will need to purchase and maintain the middleware software.
Lastly, middleware cause latency in your application. This is because middleware code needs to be executed before your application can process the request.
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.
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 pages
directory. Unlike Next 12
, where we have to add an underscore, the Next 13
doesn’t require that. This file will contain the code you want to run before a request is completed. Inside the file, paste this:
import { NextResponse } from "next/server"; export default function middleware(req) { if(req.nextUrl.pathname =="/api/hello"){ if(req.nethod != 'POST'){ return new NextResponse("Cannot access this endpoint with " + req.method, { status: 400}) } return NextResponse.next(); } }
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. The NextResponse.next()
function to tell Next.js to continue processing the request. Here’s our result:
If you’ve ever used serverless functions, you’ll understand 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.
Let’s take a look at a few ways we can use the Edge Functions.
In Next.js, Edge Functions are commonly used for data fetching, enabling you to optimize server load and enhance the performance of your web application by bringing data closer to the edge. Next.js’s Edge Functions provide the capability to fetch data, as demonstrated in this example:
export default async function handler(req, res) { const { id } = req.query; // Fetch user data from a different API endpoint const response = await fetch(`https://api.mywebsite.com/users/${id}`); const userData = await response.json(); // Return the user data res.status(200).json(userData); }
In this code example, an API endpoint is defined to fetch user data from a third-party API based on the provided ID
query parameter. The fetch
method sends a request to the external API, and the await keyword ensures that we wait for the response to be resolved.
Once the response is received, the response.json()
method is invoked to extract the JSON data, which is then stored in a variable. The data is formatted as JSON using the JSON
method, and the user data is returned as the API response with a 200
status code.
You can efficiently fetch data from external APIs by using this approach in Next.js with Edge Functions. This helps enhance your application’s performance by reducing the load on the server and delivering data closer to the edge of the network, resulting in faster and more responsive UX.
Next.js provides a convenient way to enhance the security of your web application and protect sensitive data by implementing access control rules at the edge. For instance, let’s consider a scenario where a function checks a client’s authentication state and echoes it back in the response.
Here’s a basic outline that demonstrates the authentication verification process using Next.js’ Edge capabilities:
export default (req, res) => { const { userAuthenticated } = req.cookies; if (userAuthenticated) { res.status(200).json({ message: 'user authenticated' }); } else { res.status(401).json({ message: 'user not authenticated' }); } }
In this example, the Edge Function examines the cookie to find an authentication token. If an authentication token is present, the function generates a JSON response confirming that the user is authenticated. Otherwise, if no authentication token is found, the function returns a JSON response indicating that the user is not authenticated.
This simplified explanation highlights the use of Next.js’s Edge capabilities for authentication purposes, allowing you to enhance the security of your web application and control access to sensitive data. To learn more about authentication in Next.js, check out our article on using NextAuth.js for client-side authentication in Next.js.
Next.js provides the capability to implement dynamic routing using Edge Functions. This lets you define and manage custom routes based on dynamic data, such as query parameters or request headers. Dynamic routing is handy for creating flexible and adaptable web applications that handle different requests. Here’s an example of how to implement dynamic routing in Next.js:
export default function handler(req, res) { const { slug } = req.query; if (slug === 'product') { res.status(200).send('This is the product page!'); } else if (slug === 'pricing') { res.status(200).send('This is the pricing page!'); } else { res.status(404).send('Page not found.'); } }
In this example above, our code represents the contents of a file named [slug.js]
located in the pages/api directory
. This file demonstrates the creation of a custom route that uses dynamic data. The value of the slug is extracted from the request query using req.query
, allowing you to efficiently handle incoming requests based on the value of the slug.
The code checks the value of the slug and performs conditional checks using if-else
statements. If the slug matches a specific value, such as product
, the server will respond with This is the product page!
. Similarly, if the slug is pricing
, the server will respond with This is the pricing page!
. If the slug does not match any of the defined routes, a Page not found.
response is sent with a 404
status.
This approach allows you to dynamically handle different types of requests and serve appropriate responses based on the value of the slug parameter. It gives you the flexibility to create custom routes and provide dynamic content to users based on their specific requests.
Next.js’s Edge Functions offer a valuable capability to optimize web application performance through A/B testing. A/B testing allows you to compare different versions of a website or application to identify the one that performs better. Here’s a simple demonstration of how we can use Edge Functions to carry out A/B testing:
// pages/api/abtest.js export default function handler(req, res) { // Generate a random number between 0 and 1 const randomNumber = Math.random(); // Set the A and B variants const variantA = 'Welcome to variant A!'; const variantB = 'Welcome to variant B!'; // Assign a variant to the user based on the random number const selectedVariant = randomNumber < 0.5 ? variantA : variantB; // Render the corresponding variant res.status(200).json({ variant: selectedVariant }); }
In the code above, the API endpoint /api/abtest
is created to perform A/B testing. When a user makes a request to this endpoint, a random number between zero and one is generated using Math.random()
. The user is assigned to either Variant A or Variant B based on this random number.
The code defines the content for both variants, variantA
and variantB
. If the random number is less than 0.5
, Variant A is selected; otherwise, Variant B is chosen. Finally, the selected variant is returned as a JSON response.
By using Next.js’s Edge Functions, developers can effectively conduct A/B testing to gather valuable user behavior data. This data-driven approach allows for iterative optimizations, ultimately enhancing the overall performance and UX of the web application.
Cache responses are a powerful feature of Next.js’s Edge capabilities that can optimize web performance. By storing responses for a specific duration, we can minimize server requests and improve the speed of the web application. Here’s an example of how you can implement response caching using Edge capabilities in Next.js:
// pages/api/data.js const userData = { name: 'annonymous', age: 25 }; export default function handler(req, res) { // Set response headers to enable caching res.setHeader('Cache-Control', 's-maxage=10, stale-while-revalidate'); // Return data res.status(200).json(data); }
In this code example, the data.js
API endpoint demonstrates the use of Next.js Edge capabilities for response caching. The code sets the response headers using the res.setHeader
method to enable caching. The Cache-Control
header is configured with the value 's-maxage=10, stale-while-revalidate'
, allowing the CDN to cache the response for up to ten seconds before requiring a refresh.
The code then returns a JSON response containing some data the data object represents. By implementing response caching, developers can enhance the performance of their web applications by reducing server requests. Users can experience faster and more responsive websites, as the cached responses are served promptly, ensuring a smoother browsing experience.
Understanding how to use middleware with Edge Functions efficiently solves the issue of sharing a common login across multiple applications, such as 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. Now that we’ve done that, let’s create our middleware. Let’s create a file called middleware.js
at the same level as our pages folder. 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 if it 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/apis
folder, create a protected
folder. Then, create an index.js
folder inside it and past this:
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 }); }
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 talked about Edge Functions, how to create one, and its use cases. Also, we implemented a basic password authentication using middleware and Edge Functions.
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 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 is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
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.
Would you be interested in joining LogRocket's developer community?
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 nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
3 Replies to "Using Next.js’ middleware and Edge Functions"
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?