Middleware in Next.js
The approaches without a custom server
Next.js recently released version 9, which added API Routes. This enabled us to write... well... APIs.
This was a good excuse to move away from frameworks like Express as a custom server.
Still, something was missing: Middleware. This is my attempt to replace it.
Middleware
Lots of us have probably learned the concept of middlewares when we worked with Express or Connect. The concept allowed us to augmented req
and res
by routing them through layers of a stack, which are known as middleware
.
The usage is somewhat like below:
app.use((req, res, next) => { // Augmenting req req.user = getUser(req); // Go to the next layer next();});
More often, we find ourselves using libraries:
app.use(passport.initialize());
In those cases, the libraries actually return functions of (req, res, next)
just like the way we approached above.
However, in Next.js API Routes, we do not have such ability. We can only export a function of (req, res)
, there is no interfaces of app.use()
. This limitation led people back to using Express, thus rendering API Routes useless.
Possible solutions
Luckily, there are ways to achieve the similar behavior that is in Express.
Let's write some middles.
If you are unsure of which approach to go with, I recommend using next-connect.
Wrappers around handler function
I will define handler function as the function of (req, res)
that we need to export for API Routes. This approach is similar to Higher-order component
(HOC) from React.
Making a middleware as a wrapper
I will take an (simplified) example from my project nextjs-mongodb-app. (check it out too)
const withDatabase = (handler) => { return async (req, res) => { await client.connect(); req.db = client.db("somedb"); return handler(req, res); };};
export default withDatabase;
Looking at the function withDatabase
, it accepts an argument called handler
, our original function. withDatabase
actually returns a function of (req, res)
(return async (req, res)
), which will accept the incoming requests. We can say that it replaces the original (req, res)
at this point.
Looking at the part:
req.db = client.db("somedb");
The incoming request (the original req
object) does not have db
, and we are adding it. Particularly, we are assigning db
into req
so we can access it later.
Now that we have augmented req
, we want to route it through our original handler
. Looking at return handler(req, res);
, we are calling the original handler
function we retrieve as an argument with the augmented req
and (eh, unchanged) res
.
Now in my original handler
, I can use the pass-along db
.
const handler = async (req, res) => { const user = await req.db.findOne({ userName: req.body.username }); res.send(`Our homie is ${user.name}`);};
Remember that withDatabase
needs handler
. We simply do withDatabase(handler)
. We now export default like so:
import withDatabase from "../middleware/withDatabase";
const handler = async (req, res) => { const user = await req.db.findOne({ userName: req.body.username }); res.send(`Our homie is ${user.name}`);};
export default withDatabase(handler);// instead of export default handler;
What about an additional option? Let's say I want to specify the database to use. We can simply add it as the second argument. Let's rewrite our withDatabase
.
const withDatabase = (handler, dbToUse) => { return async (req, res) => { await client.connect(); req.db = client.db(dbToUse); return handler(req, res); };};
Now back to our API Route file:
export default withDatabase(handler, "testingDb");
Obviously, you can add as many arguments as you want, we only need to make sure to pass along our original handler
.
Multiple middlewares
What about multiple middlewares? We can write similar functions to useDatabase
. Let's say we want a middleware to check the database's readiness.
const withCheckDb = (handler) => { return async (req, res) => { req.dbHealth = await checkDatabase(req.db); return handler(req, res); };};
Now that we have our additional withCheckDb
, we can wrap it along with withDatabase
.
export default withDatabase(withCheckDb(handler), "testingDb");
One thing to be aware of is that withCheckDb
is inside withDatabase
. Why?
Looking at withCheckDb
, we see that it tries to access req.db
, which is only available after withDatabase
. The function on the outside will receive req
and res
first, and only when they are done that they pass them along into the inside ones.
So, order matters.
Stop the middleware chain early
Let's take another look at our withCheckDb
. What would happen if our database is not working? In such a case, I want it to simply respond with Database is not working
, ideally with a 500 status code. In Express.js, we often achieve this by not calling next()
. Similarly, in this approach, we don't call the original handler function.
const withCheckDb = (handler) => { return async (req, res) => { req.dbHealth = await checkDatabase(req.db); if (req.dbHealth === "bad") return res.status(500).send("Database is not working :( so sorry! "); return handler(req, res); };};
If the result of our checkDatabase
is bad, we send the message "Database is not working". More importantly, we also return
at that point, exiting the function. return handler(req, res);
is not executed because the function has existed/returned earlier.
By doing so, the actual handler
never run, thus the chain is cut short.
Mutate req and res directly
Another approach to use middleware is to manipulate req
and res
directly. We can try to rewrite the above functions withDatabase
and withCheckDb
using this approach.
const useDatabase = async (req, res, dbToUse) => { await client.connect(); req.db = client.db(dbToUse);};
Instead of getting a handler
, we instead take req
and res
as arguments. Actually, we do not even need res
because we do not mutate it.
const useDatabase = async (req, dbToUse) => { await client.connect(); req.db = client.db(dbToUse);};
Let's go back to our handler.
import useDatabase from "../middleware/useDatabase";
const handler = async (req, res) => { await useDatabase(req, "testingDb"); const user = await req.db.findOne({ userName: req.body.username }); res.send(`Our homie is ${user.name}`);};
export default handler;
By calling await useDatabase(req, 'testingDb');
, we mutate our req
by injecting our db
into it. I need to use await
because we need to wait for client.connect()
, followed by setting req.db
.
Without await
, the code will go on without req.db
and end up with a TypeError req.db is not defined
.
Multiple middleware
Let's do the same thing with withCheckDb
:
const useCheckDb = async (req, res) => { req.dbHealth = await checkDatabase(req.db); if (req.dbHealth === "bad") return res.status(500).send("Database is not working :( so sorry! ");};
We need res
in this case since we call calling res.send
.
We can then go on to use multiple middlewares like so:
import useDatabase from "../middleware/useDatabase";import useCheckDb from "../middleware/useCheckDb";
const handler = async (req, res) => { await useDatabase(req, "testingDb"); await useCheckDb(req, res); const user = await req.db.findOne({ userName: req.body.username }); res.send(`Our homie is ${user.name}`);};
export default handler;
Stop the middleware chain early (approach 2)
Remember that we want to stop the code if the database is not working. However, it does not just work with this approach.
useCheckDb
will still call res.status(500).send('Database is not working :( so sorry! ')
, but then the code go on. Chances are that the code will throw at req.db.findOne({ userName: req.body.username })
, or you will end up with Can't set headers after they are sent to the client
when you try to res.send(`Our homie is ${user.name}`)
.
One way is to intentionally throw an error inside useCheckDb
const useCheckDb = async (req, res) => { req.dbHealth = await checkDatabase(req.db); if (req.dbHealth === "bad") throw new Error("Database is not working :( so sorry! ");};
...and catch it with a Try/Catch.
import useDatabase from "../middleware/useDatabase";import useCheckDb from "../middleware/useCheckDb";
const handler = async (req, res) => { try { await useDatabase(req, "testingDb"); await useCheckDb(req, res); const user = await req.db.findOne({ userName: req.body.username }); res.send(`Our homie is ${user.name}`); } catch (e) { res.status(500).send(e.message); }};
export default handler;
e.message
, in this case, will be "Database is not working :( so sorry!".
Middleware with next-connect
The two above approaches did not settle me, so I decided to write next-connect that will take me back to good ol' Express.js.
With next-connect
, we can now use Express middleware syntax like we used to be.
import nextConnect from "next-connect";const handler = nextConnect();
handler.use(function (req, res, next) { // Do some stuff with req and res here req.user = getUser(req); // Call next() to proceed to the next middleware in the chain next();});
handler.use(function (req, res) { if (req.user) res.end(`The user is ${req.user.name}`); else res.end("There is no user"); // next() is not called, the chain is terminated.});
// You can use a library too.handler.use(passport.initialize());
export default handler;
Method routing, too
What even better is that next-connect
also takes care of method handling. For example, you may want POST
request to be responded differently to PUT
request.
handler.post((req, res) => { // Do whatever your lil' heart desires});
handler.put((req, res) => { // Do whatever your lil' heart desires});
export default handler;
Example usage with next-connect
Anyway, let's get back on track. Let's try to replicate use/withDatabase
and use/withCheckDb
.
function database(dbToUse) { return async (req, res, next) => { await client.connect(); req.db = client.db(dbToUse); // Calling next() and moving on! next(); };}
function checkDb() { return async (req, res, next) => { req.dbHealth = await checkDatabase(req.db); if (req.dbHealth === "bad") return res.status(500).send("Database is not working :( so sorry! "); next(); };}
The writing of the two functions are similar to our first approach. The only differences are that:
- We do not need to take in a
handler
argument - Our returned function has an additional
next
argument. - We finish by calling
next()
instead of callinghandler
.
What about suspending the code if checkDb
fail? Similarly to the first approach, next()
will not be called and whatever comes after it will not run.
For instruction on writing middlewares, here is a guide on expressjs.com.
Now, we can use it like we do in the old days of Express.js.
import nextConnect from "next-connect";import database from "../middleware/database";import checkDb from "../middleware/checkDb";
const handler = nextConnect();
handler.use(database());handler.use(checkDb());handler.get((req, res) => { const user = await req.db.findOne({ userName: req.body.username }); res.send(`Our homie is ${user.name}`);});
export default handler;
Conclusion
I hope this will help your effort on moving away from Express.js
. Moving away from Express.js
will allow our app to run faster by enabling Next.js
's optimization (and serverless, too!).
If you have any questions, feel free to leave a comment or DM me on Twitter.
Good luck on your next Next.js project!