mg website logo

Writing Express Middleware

January 27, 2020

When writing an API with Express.js, understanding middleware is critical. Middleware allows you perform actions on requests.

In this tutorial, we’ll learn about what middleware is and create a basic middleware function that logs each request.

Setup

To get started, we’ll need to create a new project and add Express as a dependency.

$ npm init
# Hit enter to proceed with defaults.
# You can always edit your package.json later.
$ npm install express --save

After installing express, we will create a new file called index.js, with the code below.

index.js

const express = require('express')

const app = express()
const port = 3000

app.get('/hello', (req, res) => {
  res.status(200).send('Hello World.')
})

app.listen(port, () => {
  console.log(`express server listening on port ${port}`)
})

This will run an express server on port 3000. It has one route, GET /hello, that will return the text Hello World. and an HTTP status of 200 when we call it.

To start the server, run:

$ node index.js

As we update index.js throughout the tutorial, don’t forget to stop the node process (ctrl + C), and run it again.

Now that we have an express server running, let’s verify that it’s working. In your browser visit http://localhost:3000. This will make a GET request to our server, and we should see our ‘Hello World’ message.

Our First Middleware

Middleware in Express is extremely powerful. We can use it to run code, modify requests and responses, or terminate a request.

It may not be obvious, but we’ve actually already written our first middleware in the server we created above!

app.get('/hello', (req, res) => {
  res.status(200).send('Hello World.')
})

Almost everything you will define with Express routing will be middleware. In this example, our middleware will only run when there is a GET request made to the /hello route. This middleware is quite simple, and returns a status with some text.

What is a Middleware Function?

The middleware above is a simplified version of what is available to us in a middleware function.

The full function definition looks more like this:

app.get('/hello', (req, res, next) => {})

Broken into it’s components, we have:

  • .get - what HTTP method our middleware applies to.
  • '/hello' - The route that this middleware applies to.
  • (req, res, next) => {} - the middleware function itself.
  • req - The HTTP request object that is supplied to the function, usually called ‘req’.
  • res - The HTTP response object that is supplied to the function, usually called ‘res’.
  • next - the callback argument for the middleware function, usually called ‘next’.

Note: An important thing to keep in mind is that every middleware must do one of the following:

  • End the request-response cycle
  • call next() to continue executing the next middleware function

In our example above, we end the request by calling res.status(200).send('Hello World.'). We’ll look at using next() later in this article.

Defining a Logging Middleware

Now that we understand each component of middleware, let’s write one that logs each request made to our server. This is a common use case, especially when debugging your server.

We’ll declare a function that we use as a logger. It will take in three arguments, as we saw above.

const logger = (req, res, next) => {}

Now let’s log something each time this function is called.

const logger = (req, res, next) => {
  console.log('A request was made!')
}

To complete our middleware, we need to call the next callback that was supplied as an argument to our function.

const logger = (req, res, next) => {
  console.log('A request was made!')
  next()
}

The next callback works as a way to control the flow of middleware. Express provides this callback to each function, so that we can let it know that we’re done, and other code can continue running.

By calling the next function, we’re saying that this function has finished executing, and we are passing control to the next middleware.

If we forget to call next here, our request will hang, and there will be no response to the client.

Applying Our Middleware

Now that we’ve defined our new middleware, called logger, we need to use it!

Notice how we never defined a route to the logging middleware when we wrote it? We can do that now:

app.get('/hello', logger)

This will apply our middleware to any request made to the path /hello.

Applying Middleware to All Routes

What if we want to apply middleware to every route? A logging middleware is probably most effective if it runs on every request, regardless of the route.

To apply our middleware on every request, we’ll take advantage of app.use:

app.use(logger)

That’s it! Now our middleware will respond to every request.

Try it out:

  • Stop the server currently running (ctrl + C) and restart it node index.js
  • In your browser refresh the page at http://localhost:3000/hello. You should see a new line in your console that says ‘A request was made!‘.

Customizing the Logger Middleware

Knowing when a request is made is useful, but what if we want more info?

Let’s add the path that each request was made to.

const logger = (req, res, next) => {
  console.log(`A request was made to: ${req.path}`)
  next()
}

Now restart the server and refresh your page at http://localhost:3000/hello. You should see a new line in your console saying ‘A request was made to /hello’.

What happens if you make a request to a different path? Try http://localhost:3000/another/path. Even though you may not have a route defined for that path, you’ll still see the request was logged in your console!

Timestamps are often important when debugging requests. Let’s add a time to each log:

const logger = (req, res, next) => {
  const timestamp = new Date()
  console.log(`${timestamp} A request was made to: ${req.path}`)
  next()
}

Now when you reload the server and refresh your page, you’ll see a log that includes a timestamp and the route you visited. For example, Mon Jan 27 2020 16:03:02 GMT-0800 (Pacific Standard Time) A request was made to: /hello

Since this middleware function is just JavaScript, we can add any code that we want here!

To practice, here are a few things to try implementing:

  • display the date in a more readable format
  • show the HTTP method that was used to make the request
  • show the HTTP version that was used to make the request
  • show the headers attached to this request

The Complete Example

Here’s what our code looks like now that we’ve added a logger.

const express = require('express')

const app = express()
const port = 3000

const logger = (req, res, next) => {
  const timestamp = new Date()
  console.log(`${timestamp} A request was made to: ${req.path}`)
  next()
}

app.use(logger)

app.get('/hello', (req, res) => {
  res.status(200).send('Hello World.')
})

app.listen(port, () => {
  console.log(`express server listening on port ${port}`)
})

View as Gist on GitHub

Note: order matters – the middleware that comes first will be executed first.

A Note on Next

As we discussed above, it’s important to call next() from every middleware that does not end the request-response cycle. What happens when we forget to call next()?

Let’s take a look at the following code (modified from our example above):

const express = require('express')

const app = express()
const port = 3000

const logger = (req, res, next) => {
  const timestamp = new Date()
  console.log(`${timestamp} A request was made to: ${req.path}`)
  // remove the call to next here.
  // next()
}

app.use(logger)

app.get('/hello', (req, res) => {
  res.status(200).send('Hello World.')
})

app.listen(port, () => {
  console.log(`express server listening on port ${port}`)
})

Now restart the server and refresh the page in your browser. What happened?

The request never completes.

It’s not in an error state (ie. returning a 400 or 500 error), but instead the request is left open forever. Express is expecting us to call next() after our middleware finishes executing. Since we forgot to call it, Express assumes we’re still running code and it waits indefinitely for next() to be called.

The logger still functions, but never passes off control to the next middleware. We can see the line printed to our console stating that a request was made, but we don’t get a response on the browser.

This can be a very difficult scenario to debug. If you’re working with an Express server and have requests that fail to complete, try checking that all of your middleware are properly terminating a request or calling next().

Additional Uses of Middleware

We’ve built a logger with middleware that displays info from the request object, and then continues executing. What else can we do with middleware in Express?

Since we have access to the req and res objects, we can use them to get creative with middleware. We can add properties to the request object, and then use those properties later in other middleware functions. We can close requests if they don’t have valid headers (like Authorization, for example).

What’s Next?

There’s a lot more to middleware. Here are some great resources to continue learning:

Note: If you’re looking for a pre-built solution to logging middleware in Express, you should check out morgan, a logger maintained by the Express team. It’s easy to use and has a lot of functionality built in.

Congrats on building your own middleware and learning more about Express! 💫

If you have any questions, comments, or corrections email me at mike@mguida.com.


Written by Mike Guida who enjoys prolonged outdoor adventures and building stuff with software. Follow him on Twitter