Next.js, Express.js, JavaScript

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 calling handler.

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!