I'm currently moving my site to Next.js from a site that used an Express backend. I'm a huge fan of Express, and one of the things I love about Express is how simple it is to implement middleware.
Middleware is a function that you can attach to routes in order for it to run before the route. In other words, it runs in "the middle." This is useful for things like checking if a user is authenticated or has the proper roles to access a route.
These are features you would want on multiple routes, and middleware makes it easy to write the function in one place and reuse it across multiple routes (Don't Repeat Yourself). But what about Next.js–how do you implement middleware?
Well, you could create a custom Express server to work with Next.js, but you're losing some of the benefits of creating your API the way Next.js intended.
Instead, you can watch how I implement middleware in my Next.js application in the video below, or for those that rather read I have a written explanation below the video–enjoy! 😊
In Express I have all my routes and middleware associated with authentication inside an
authController.js file. One of those middleware functions is the
protect function, which you can see in the code below:
This protect function would check to see if the
req had cookies and if the
st_accessToken cookie was present. If so, it would attach the token to the
token variable defined at the top of the function.
If no token was present my application would return an error asking the user to log in, so the user would never reach the final route. If a token was present the function would then proceed to run some code in a
try / catch block.
try block the token would be decoded using the
jwt.verify method from the
jsonwebtoken package. This method is passed the token and my application's JWT secret. Once that's decoded my application has access to the user's unique ID on
decoded.id. This user ID is then used to make a call with Mongoose to my MongoDB database:
const currentUser = await User.findById(decoded.id);
If no user if found the application will return an error, otherwise the function will attach a
user object on
req based on the
currentUser that came back from MongoDB. Last, unless an error is caught in the
catch block, the function calls
next(), which tells Express to move to the next route handler or middleware.
Now routes or middleware further down the chain will have access to the user object and can use it however they want. For example, my routes for Stripe will now be able to read the Stripe Customer ID from the user object that is attached to
This middleware is implemented in Express when I create my routes in the
In Next.js you can create your routes in a folder called
api, which needs to be inside the
pages folder. Then, inside your
api folder you can create all your route handlers, nesting them inside other folders based on how you want your API to be organized. Next.js will handle creating the routes for you, so there's no need to define them like you would in Express.
For example, a
logout.js handler inside pages > api > users > logout can be accessed in development from
localhost:3000/api/users/logout. Pretty neat, right?
However, because Next.js handles the routing for us, we can't just pass a middleware before the route is called when we define routes ourselves in Express. So now let's look at the withProtect middleware code:
Look familiar? That's because it's almost identical to the protect middleware in Express! However, there are some important things to point out. To make them easier to see, check out this code example below where I remove some of the identical code:
You can now see how this withProtect middleware takes the handler in as an argument, then returns a function of
(req, res). This essentially takes over the handler for now, before it later passes it back into the original handler when
handler(req,res) is returned.
Now with the withProtect middleware complete, it's time to implement it inside the logout route. Refer to the following code:
Inside the logout handler you can see that it has access to
req.user, which is passed on by the withProtect middleware.
So how does the logout handler get the access? Well, if you look at the bottom of the code you will see that I wrapped the exported handler with the withProtect middleware–identical to how you would do higher order components in React:
export default withProtect(handler);.
By doing this, the withProtect middleware will run before the logout handler and give the logout handler the user object on
req.user, unless there is an error and the middleware will return an error.
What happens if you want to add multiple middleware on a handler? Simple, you just nest it inside the other middlewares!
For example, check out this withRoles middleware:
This middleware takes two arguments: the original handler function and a string of possible roles that can access the route.
Users are assigned the role of
user in my database by default when they're created. This is important because there are some routes, like deleting a user, that I only want users with the role of
admin to access.
This withRoles middleware gets access to the user on
req.user because it's nested inside the withProtect middleware. Although it doesn't make much sense to only allow admins to logout, check out this simplified example of the logout handler:
First, the withProtect middleware runs, either attaching the user object on
req.user or returning an error. Then the withRoles middleware checks to see if the req.user.role matches 'admin'. If it does, the logout handler is then called. Otherwise, the middleware returns a response notifying the user that they do not have the proper permissions.
Nesting middleware like this can look a little weird compared to how you would implement it in Express, but once you get the hang of it the implementation in Next.js is simple.
Found this helpful? Subscribe to my YouTube channel