Express Middleware

Processing Data Before Responding To A Request

What is middleware and why do I care?

Middleware is a concept in web application development in which we execute some behavior in the middle (that's why it's called that) of the Request => Response cycle that happens between the client and the server. Essentially, middleware allows us to process our request code in some way before sending it to our application for processing and ultimately sending a response to the requesting agent.

Middleware is used for all sorts of things. It is most popularly used to authenticate a user or scrub input before sending it along to our application. This middle step makes putting nefarious code into our application that much harder.

Middleware operates as small applications that do some sort of preprocessing with our data so that our application doesn't have to worry about doing it. This is why you see so many third party middleware libraries being plugged in to your Express applications. We have trusted libraries doing the job of logging, parsing our request body, handling cookies, etc. What if there is a specific use case for our application? Let's explore that.


Example Project

Follow along here

In our example project we have routes to display all games, a single game, all studios, and a single studio. We want to be able to add a new game to our application.

The Problem

We need to retrieve an existing studio to add to our new game. We could handle this in the controller method for adding a new game, but what if we need to repeat this behavior? This is exactly what middleware is for. Since the process of retrieving a studio really doesn't have anything to do with adding a game to our data store, we really shouldn't include it in our controller.

The Solution

Custom middleware!


Game Router

Let's have a look at what our current game-router looks like.

const express = require('express');
const router = express.Router();

const gamesController = require('../controllers/games-controller')

/* GET games */
router.get('/', gamesController.readGames);

/* GET game */
router.get('/:id', gamesController.readGame);

module.exports = router;

We need to add a post route for adding a game. Let's make that to see what it's going to look like and then build the middleware to actually handle it.


New Games Router

const express = require('express');
const router = express.Router();

const populateStudio = require('../middleware/populate-studio')
const gamesController = require('../controllers/games-controller')

/* GET games */
router.get('/', gamesController.readGames);

/* GET game */
router.get('/:id', gamesController.readGame);

/* POST game */
router.post('/', populateStudio, gamesController.createGame)

module.exports = router;

So we're importing populateStudio from a new middleware directory, so we'll need to create that. We also have a new method on our gamesController, createGame so we'll need to make that as well. Let's start with the middleware.


populateStudio

const studiosService = require('../data/studios-service')

module.exports = (req, res, next) => {
  const studioId = req.body.studioId
  const studio = studiosService.findById(studioId)

  if (studio === undefined) res.status(500).json({ message: "Requested studio doesn't exist" })

  req.body.studio = studio

  next()
}

So let's dig through what this middleware is doing. The first thing to note is that we are exporting a function which takes the parameters req, res, and next. The first two should be familiar at this point, but for what is next used? Let's circle back to that.

We're also importing the studiosService. This is going to give us access to be able to actually pull studios into our application context.

First, we're retrieving the authorId from our request body and then using it to retrieve and actual studio from our service.

Next, we're validating that the requested studio exists and sending an error if it doesn't.

Next, if the requested studio does exist, we're assigning it to a value in the request body.

Finally, we're calling the next method which tells our application that this middleware is finished and to move on to the next function in the chain.

Now we can use this value in our controller!


Games Controller

Now that we have this middleware operating properly, we can move on to what our controller actually needs to do. Let's add the createGame method.

const gamesService = require('../data/games-service')
const studiosService = require('../data/studios-service')

const idGenerator = require('../utils/id-generator')
const Game = require('../models/Game')

module.exports = {
  createGame (req, res) {
    const id = idGenerator()
    const title = req.body.title
    const description = req.body.description
    const studio = req.body.studio

    gamesService.save(new Game(id, title, description, studio))

    res.redirect('/games')
  },
  readGames (req, res) {
    res.render('games/all', { games: gamesService.findAll(), studios: studiosService.findAll() })
  },
  readGame (req, res) {
    res.render('games/single', { game: gamesService.findById(req.params.id) })
  }
}

So createGame is first generating a new id from our utility method. Next, it's just pulling values from the request body (including the studio from our middleware!) and using it to make a new Game object and passing it to a new method in the gamesService, save. Let's see what that looks like.


gamesService

const { games } = require('./db-mock')

module.exports = {
  findAll () {
    return games
  },
  findById (id) {
    return games[id]
  },
  save (game) {
    games[game.id] = game
    return games[game.id]
  }
}

The save method is fairly straight forward. It accepts a game object as a parameter and uses that games "generated" ID as the key and assigns the Game object as the value.


Conclusion

Middleware is a great tool to keep code decoupled. It also makes this sort of behavior repeatable without having to write it our multiple times. This is the kind of tool that will elevate your code cleanliness if you can get good at it. Happy coding!