Use middleware in Next.js without custom server

hoangvvo

Hoang

Posted on October 19, 2019

Use middleware in Next.js without custom server

Three months ago, Next.js released version 9, which added API Routes. This enabled us to write... well... APIs by exporting functions of two arguments req and res, which are extensions to Node's http.ClientRequest and http.ServerResponse.

This was a good excuse to move away from frameworks like Express as a custom server.

Still, something was missing: Middleware.

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 my 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.

Concept illustration in React

To illustrate the concept, I might use the term Higher-order component (HOC) from React (even though it is a little bit misleading). If you use React, you may know the technique as something like the one below:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

higherOrderComponent will be something like below:

function higherOrderComponent(OriginalComponent) {
  const user = getUser();
  return (<OriginalComponent user={user} />);
}

In the above example, we wrap WrappedComponent with higherOrderComponent. Like the variable name suggests, it returned an enhanced version of the original component, not a different one. The only difference was that it added a prop to the original component. The HOC augmented the original component, not mutate it.

We can apply the concept into making a "HOC" for API Routes.

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 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. You can look at another example at how I have options in next-session.

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.

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 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

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 a library that will take me back to good ol' Express.js.

You can get it here.

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;

What about non-API pages

We have been talking about API Routes (those in pages/api), what about normal pages (those in pages/). We can apply approach 2 to getInitialProps.

Page.getInitialProps = async ({ req, res }) => {
  await useMiddleware(req, res);
  /* ... */
}

Document middleware

An RFC in Next.js issue #7208 enables the same approach as above but allowing it to be available globally.

It is an experimental feature and need to be enabled in nextjs.config.js:

module.exports = {
  experimental: {
    documentMiddleware: true
  }
};

Then, in _document.js:

export const middleware = async ({ req, res }) => {
  await useMiddleware(req, res);
};

Using next-connect

See this.

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. I also recommend asking on Next.js channel on Spectrum to get answers from great people there.

Good luck on your next Next.js project!

💖 💪 🙅 🚩
hoangvvo
Hoang

Posted on October 19, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related