Module 3 - Express Middleware

What is Middleware?

There are different types of middleware; for our purposes, we'll group them into three different categories:

  • Built-in middleware
  • Third-party middleware
  • Custom middleware

Built-in Middleware

Built-in middleware is included with Express but not added to the application automatically. Like the other types, we need to opt-in to using it in our application.

We saw an example of built-in middleware when we added support for parsing JSON content out of the request body using server.use(express.json());.

Every type of middleware works in the same way. We tell Express about the middleware we want to turn on for our application by making a call to .use() on our server and passing .use() the piece of middleware we want to apply. This line must come after the server has been created by calling express().

Third-party Middleware

Third-party middleware are npm modules that we can install and then import into our applications using require(). There are thousands of middleware modules we can use. There is no need to write our own in most cases.

Some popular middleware modules are:

Custom Middleware

Custom middleware is functions we write to perform specific tasks. We'll learn more about how to write and use them in the next section.

How to Build It

One thing that is not immediately obvious is that route handlers are middleware. Let's see this in action.

Suppose a client visits a non-existent endpoint in our current implementation. In that case, they will get a default message when a resource is not found on the server. In the case of a browser, it's Cannot Get /urlWeTriedToAccess. This default message makes for a poor user experience. So please code along as we write a request handler that responds with a custom message for invalid URLs.

function(req, res) {
  res.status(404).send("Ain't nobody got time for that!");
};

This code is not complete yet, but you can see that it is, in fact, a request handler function. We know because the homies (req and res) are there.

Now let's just use it as if it was middleware:

server.use(function(req, res) {
  res.status(404).send("Ain't nobody got time for that!");
});

Almost there! The last step is adding this status after each route handler. That way, if the preceding middleware or route handlers do not respond to the request, this will become our catch-all and respond with the correct HTTP status code and a helpful message.

Now, requests to non-existent endpoints will result in a better experience for our clients.

Write Custom Middleware

Writing custom middleware is a two-step process:

  1. Write a function that will receive three or four arguments.
  2. Add it to the middleware queue.

Let's tackle the first part with an example. We'll write middleware that logs information about every request that comes into our server. We'll be displaying the information in the console window to keep things simple.

function logger(req, res, next) {
  console.log(
    `[${new Date().toISOString()}] ${req.method} to ${req.url} from ${req.get(
      'Origin'
    )}`
  );

  next();
}

We can see that a middleware function takes three parameters:

  • the request object
  • the response object
  • a function that points to the next middleware in the queue

By convention, we name the third parameter next. Please stick to that convention in your code.

Any middleware in the queue CAN modify both the request and response objects, but it's NOT required. So, in this case, we are not making changes to either.

Any middleware in the queue can stop the request and send a response back to the client. When that happens, the rest of the middleware, including the route handlers, will not work. We'll see an example of this in the code-along section.

Calling the next() function signals to Express that the middleware has finished, and it should call the next middleware function. If next() is not called and a response is not sent back to the client, the request will hang, and clients will get a timeout error. So, make sure always to call next() or use one of the methods that send a response back like res.send() or res.json().

Now let's add our shiny middleware to the queue! Right after server.use(express.json()); add the following line.

server.use(logger);

Hitting any of our endpoints will display some information about the request in the console.

Congratulations, you know how to write custom middleware for Express!

How to Build It

Any middleware in the queue can stop the request and send a response back to the client. The rest of the middleware, including the route handlers, will not be executed when that happens. Let's see this in action by writing our very own authentication middleware, but first, a bit of a story (any similarity to an actual story is a pure coincidence).

Imagine you're next to a lake where a dangerous creature dwells, and it is moving towards you with ill intentions. Luckily, next to you is a sealed door that leads to safety. But, unfortunately, to open the door, you need to provide the right password. So let's implement the API for that.

Start by defining a function that shows our current predicament at the console as the application loads.

function atGate(req, res, next) {
  console.log('At the gate, about to be eaten`);

  next();
}

Then add it as the first middleware in the queue.

server.use(atGate);

This middleware is what's called global or application-wide middleware. It applies to every endpoint in our server. Therefore, accessing any route in our server should display the message on the console.

Now let's add the authentication middleware that only grants access if we hit the correct route; picking any other route is futile, so be careful!

function auth(req, res, next) {
  if (req.url === '/mellon') {
    next();
  } else {
    res.send('You shall not pass!');
  }
}

Now let's add a route handler that leads to safety:

server.get('/mellon', auth, (req, res) => {
  console.log('Gate opening...');
  console.log('Inside and safe');
  res.send('Welcome Traveler!');
});

What's new here is that we are adding our middleware as the second parameter and the route handler as the third. Using middleware this way is what we call local middleware or route middleware. It just means we are using middleware locally and only applies to the endpoint where it's added.

Write Error Handling Middleware

When our application encounters an error in the middle of executing middleware code, we can choose to hand over control to error handling middleware by calling next() with one argument. It is a standard convention to make that argument be an error object like this: next(new Error("error message")).

This middleware type takes four arguments: error, req, res, and next. We pass the first argument when calling next(new Error('error message here')). When the error handling code is finished, we can choose to end the request or call next without arguments to continue to the next regular middleware.

Error handling middleware can be placed anywhere in the stack, but it makes the most sense to put it at the end. Suppose the intention is for middleware to handle errors that may occur elsewhere in the queue. In that case, it needs to run after the rest of the middleware has run.

How to Build It

Let's see this error-handling middleware in code. First, let's write an endpoint that sends a file to the client in response to a GET request to the /download endpoint.

const express = require('express');
const path = require('path');

const server = express();

server.get('/download', (req, res) => {
  const filePath = path.join(__dirname, 'index.html');
  res.sendFile(filePath);
});

server.listen(5000);

If we run our server and make a GET request to /download, the server will crash since there is no index.html file to send. We need to rewrite our endpoint and take advantage of the fact that res.sendFile supports passing a callback function as a second argument. This callback function will run after the file is sent. It will also run if there is an error in the process of sending the file.

// note we added the third parameter here: next
server.get('/download', (req, res, next) => {
  const filePath = path.join(__dirname, 'index.html');
  res.sendFile(filePath, err => {
    // if there is an error the callback function will get an error as it's first argument
    if (err) {
      // we could handle the error here or pass it down to error-handling middleware like so:
      next(err); // call the next error-handling middleware in the queue
    } else {
      console.log('File sent successfully');
    }
  });
});

Now let's add error-handling middleware to our server. We can create the middleware function and then use it like any other middleware or do it inline. Below an example of using it inline.

server.use((err, req, res, next) => {
  console.error(err);

  res
    .status(500)
    .json({ message: 'There was an error performing the required operation' });
});

This middleware will only get called if any other middleware or route handler that comes before it has called next() with an argument like in the /download endpoint above.

The complete code for our server now look like so:

const express = require('express');
const path = require('path');

const server = express();

server.get('/download', (req, res, next) => {
  const filePath = path.join(__dirname, 'index.html');
  res.sendFile(filePath, err => {
    // if there is an error the callback function will get an error as it's first argument
    if (err) {
      // we could handle the error here or pass it down to error-handling middleware like so:
      next(err); // call the next error-handling middleware in the queue
    } else {
      console.log('File sent successfully');
    }
  });
});

server.use((err, req, res, next) => {
  console.error(err);

  res
    .status(500)
    .json({ message: 'There was an error performing the required operation' });
});

server.listen(5000);

Open your browser and visit http://localhost:5000/download, and the error message coming from our error-handling middleware should display.

Module 3 Project: Express Middleware

The module project contains advanced problems that will challenge and stretch your understanding of the module's content. The project has built-in tests for you to check your work, and the solution video is available in case you need help or want to see how we solved each challenge, but remember, there is always more than one way to solve a problem. Before reviewing the solution video, be sure to attempt the project and try solving the challenges yourself.

Instructions

The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:

Starter Repo: Node API 3

  • Fork the repository,
  • clone it to your machine, and
  • open the README.md file in VSCode, where you will find instructions on completing this Project.
  • submit your completed project to the BloomTech Portal

Solution

Additional Resources