Next.js — Middleware & Edge Function

Raj Chaudhary
readytowork, Inc.
Published in
8 min readMar 23, 2024

Middleware in Next.js is implemented a bit differently than in other frameworks like Express, Middleware is a function that runs before a request is completed. So before the API route is completed we need to run the application. We need to run the application before the API route function is executed or before the code for a page is run. We can modify the response based on the user’s request. For example, we can get the user’s geolocation from the request and then redirect the user based on that or we can authenticate the request if it is a user of admin as per need.

Middleware is just functions that run in between the request and the response. These functions will have access to the request and the response objects, just like how in our next API handler right here, this function has access to these same objects. What middleware does is that it can either change, redirect, or even end an HTTP request. So if a user needs to be authenticated before accessing an endpoint, we won’t let the user even get to that endpoint because the request will come in, it’ll go to our middleware first, and our middleware will check if the user is authenticated or not.

And if they’re not, they’re going to simply end the request or redirect them to the login screen. Middleware can also be chained together, so you can have multiple middleware functions running on a single route or before a single route.

Next.js middleware is a bit special because it runs on the edge. This means that the functions are deployed on multiple servers across the globe. It makes it fast. So if a user makes a request, the edge function is going to run on a server that’s closest to them geographically. So yes, this creates really fast responses, but it also comes with limitations,

If you deploy your application to Vercel, the middleware will run on the edge functions. If you want to host your application somewhere else that is possible because the middleware works out of the box using the next start. Okay, so that was middleware. Now what’s the edge then?

The edge functions well the best way to explain what edge functions are is to compare them to serverless functions. Let’s first recap how serverless functions work. So when you deploy a serverless function to Vercel it gets deployed to a server somewhere in the world. For example in Tokyo japan. Then requests to that function will be executed on the server in Tokyo japan. So if the request is made somewhere geographically close to Tokyo then the request will be pretty fast. But if you are requesting that function somewhere far away from Tokyo, let’s say, for example, in Helsinki Finland, then the request will be much slower. And this is where edge functions can help us.

So in simplicity, edge functions are serverless functions that run geographically close to the user, making the request very fast regardless of whether you are in Tokyo japan, or Helsinki.

The first thing to do to get started with middleware in Next.js is to create a middleware.js file at the same level as your page’s directory. So no underscoring.

I’m going to create it in the source because that’s where my pages folder is. So I’m going to create a new file called middleware.js. What we want to do here is export a function called middleware from this file. So we can say export function middleware. And then we pass in our request. How do we get a response? How do we return something from the middleware or allow the request to go on to the endpoint if a user has passed this middleware function? We need to import the NextResponse because we don’t have access to the response object in this function alone. So we need to import the NextResponse from the next server. That’s going to give us a response object.

Create a simple middleware that just prints hello from the middleware to the console. And the thing with middleware is after you run the function, you want to return the next thing to do. So middleware can be chained together, which is why this is important. And also, so we let the request go to the endpoint.

So let’s say return the NextResponse.next(). This is a static method on the next response, save it run it, and refresh the browser you will notice hello from middleware printed a bunch of times.

Notice this will run on every route we type into a browser. So if we do http://localhost:3000/api/hello, we’ll still get a hello from middleware. And that’s because Next.js middleware runs on every route as a default.

import { NextResponse } from "next/server";

export const middleware = (req) => {
console.log("hello from api");
return NextResponse.next();
};

export const config = {
matcher: "/api/:path*",
};

let’s say we wanted to change that. We wanted to match a middleware function to a specific URL. So maybe we only want to have the middleware run on our API routes. For that use this config that we export from this middleware file. So we can say export const config, and then it’s just an object. And inside, we can have this middleware function that we want to run on every route.

The key is called a matcher. And what we do with this is we essentially put in a string or a regex expression. You can look up the documentation on that, but the string is going to match all of our API routes. So we /api/:path*. That’s just going to match everything after this /. It’ll match because it doesn’t care. We just want all the routes that are prefixed with API.

Now, if we go back to our app and reload this, we’re going to see hello from api because we are in fact on an API route. But if we go to the root, hopefully, we won’t get that running. So that’s one way to do it.

import { NextResponse } from "next/server";

export const middleware = (req) => {
if (req.nextUrl.pathname === "/api/hello") {
console.log("hello from middleware");
}
return NextResponse.next();
};

Another way to do it is to use if statements. And that would be in the actual, middleware function itself. And in here, I can say if you request. So we have access to this object. We can get the URL. pathname and all that stuff. So the way we get the path name is by doing the url.pathname. I’m just going to say /api/hello. This is for only the hello route. And we still want to keep that return at the bottom. Reloading the page here, nothing is printed. But if we do go to that API endpoint, we should see something printed.

Let's test some code that would actually be useful in a middleware function, not just console logging. This middleware function is going to check if the request method is certain.

export const middleware = (req) => {
if (req.nextUrl.pathname === "/api/hello") {
if (req.method != "POST") {
return new NextResponse("cannot access this endpoint with GET " + req.method, {
status: 400,
});
}
}
return NextResponse.next();
};

So let’s say post. And if it’s not that method, we want to return an error and end that request. Inside of here, we access the req.method. And we say if the request method is not post, we’re going to return a new NextResponse. So if you want to send, you can also send JSON, which is helpful. And this is just passing in the options. So we can set a status to 400, which is like a bad request.

So if we reload the page, we’re going to see cannot access this endpoint with a GET. Our middleware doesn’t even allow us to get to this endpoint. It just sends back a response immediately. So if we do that, we’re going to get a response immediately. You can also put this in your function import it into this file and use it there.

Let's talk about the edge. So Next.js middleware runs on these edge functions. You cannot disable this feature. And the edge runtime is sort of its own thing. It’s not the same as a Node.js runtime. It’s its type of runtime. So you’re not going to have access to some of the things that Node.js has. One thing in particular, you’re not going to have access to a lot of NPM packages. I learned this the hard way when trying to implement authentication with Next.js middleware.

But NPM packages, those edge stuff, you can’t access within the edge runtime. So that means you can’t have any of that stuff inside of your middleware functions, or it’s going to throw an error. If you try to import JSON web token into this file and use it in this middleware function, it’s not going to work. And there are a few ways to get around this.

One, you can use a package that’s supported in the edge runtime. There is a JWT package that is supported in the edge runtime. So you can find those. Or you’re going to have to put your logic in another endpoint and call that endpoint from the middleware. That’s not the ideal option, from my understanding, because you have to make an additional request to even let the user access the original endpoint they wanted to access.

export default function middleware(req, event) {
console.log(req.nextUrl.pathname)
event.waitUntil(
fetch('http://localhost:3000/api/hello', {
method: 'GET',
}).then(async (res) => {
res = await res.json()
console.log(res)
})
)
return NextResponse.next()
}

It’s called the next fetch event. It allows you to fetch stuff in your middleware function. And then wait for it to come back, and then proceed with your other things. So this could be used in authentication if you’re making a request to your auth server and you want to wait, or also it could be a request to your API. So we pass in a request, and the event is another thing that the middleware function comes with. This is of type next fetch is say, event. Wait And this is going to wait until your fetch response comes back. So right now I’m just fetching my API. So the hello API route. It is a get request, and then we do stuff with the response turn it into JSON, and then console log it.

Refresh the page, if everything is correct then you will see the request is passed and the name is a console in your log.

--

--