Middlewares in Express.js: All you need to know

Middlewares in Express.js: All you need to know

Before talking about middleware, let's talk about what express.js is, its uses, and how it relates to middleware.

According to the official website of Express:

Express is a fast, unopinionated, minimalist web framework for Node.js.

It is a web framework built for Node.js, a JavaScript runtime built on Chrome's V8 JavaScript engine which allows JavaScript to run on the server. Express has tons of features that are useful for building backend services and APIs quickly and in a relatively easy way. It is highly performant and popular.

Additionally, Express is an entirely middleware-based framework which implies that it relies heavily on the principle of middleware for its operation. We shall see this in practice.

We'd be discussing the following:

  • What is Middleware?

  • How middleware works with an example

    • setup an express app

    • create and use a custom middleware

    • install and use a third-party middleware

  • Types of Middlewares

    • Global Middlewares

    • Route-Specific Middlewares

    • Error Handling Middlewares

What is Middleware

Middleware functions are functions that run between when a server receives a request and when it sends the response out. They have access to the request object (req), the response object (res), and the next() function in the application’s request-response cycle. They can modify the request object coming in and the response object to be sent out. With this capability, middleware can also be used to end the request-response cycle for some reason before getting to the last middleware in the middleware stack. Also, every controller passed to HTTP actions is essentially Middlewares.

Every middleware takes at least three arguments which are:

  1. ** Request object**: Often denoted as req. The req object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on. A Middleware can read any property in this request object.

  2. Response object: Often denoted as res. The res object represents the HTTP response that an Express app sends when it gets an HTTP request. A Middleware can add or modify any property in this response object before sending it out.

  3. Next function: The next middleware function is commonly denoted by next. This function is called inside a middleware for it to move to the next middleware in the Middleware call stack. Unless your current middleware is ending the request-response cycle, not calling this next function will leave the application hanging and non-responsive to any new request.

How do middlewares work?

The order of arrangement of middlewares matters a lot. Middleware are called according to how they are registered in the Middleware stack. The middleware on top is written first before the one at the bottom. To better visualize this, let's create a basic express app and add some middleware to it.

Set up an express app.

  • In your desired location, run the following to make a new directory for the app.
mkdir express-app
  • Open the directory in your code editor and open the terminal and run:
npm init -y

This will create a new package.json file for you. You would have something like this:

Screenshot 2022-04-20 at 11.47.51.png

  • Install Express
npm install express
  • In your terminal, run the code below to create an index.js file or do that manually.
touch index.js
  • Add the following code to the index.js file. The comment describes what each line does:
// import the express framework
const express = require('express');

// create an instance of the express app
const app = express();

// setup an endpoint
app.get('/', (req, res) => {
    res.send('<h1>Hello, world!</h1>');
})


const port = 8000;
// setup server to listen on port 8000
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})
  • Install nodemon as a dev dependency: nodemon is a tool that helps develop node.js-based applications by automatically restarting the node application when file changes in the directory are detected.
npm install nodemon
  • In your package.json file, under scripts, add the code below to configure the app to use nodemon:
  "scripts": {
    "start": "nodemon server.js"
  }

Now the package.json looks like this:

Screenshot 2022-04-20 at 12.05.12.png

  • Start the server by running this code in your terminal
nodemon start

To see this in action, open your browser and visit localhost:8000, you'd see something like this:

image.png

Create and use a custom middleware

This a simple middleware that logs the req object

function logger(req, res, next) {
    console.log(`Request URL is: ${req.url} and method: ${req.method}`);
    next();
}

To use this middleware in our application, we add the following line to our code:

app.use(logger);

What this does is apply the middleware to our application. For any request made to the server, the above middleware will be called.

Now your index.js looks like this:

// import the express framework
const express = require('express');

// create an instance of the express app
const app = express();

// Using the middleware
app.use(logger);

// setup an endpoint
app.get('/', (req, res) => {
    console.log("Here is the action controller")
    res.send('<h1>Hello, world</h1>');
})

// logger Middleware
function logger(req, res, next) {
    console.log(`Request URL is: ${req.url} and method: ${req.method}`);
    next();
}

const port = 8000;
// setup server to listen on port 8000
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

Note that we use the logger middleware before defining the "/" endpoint. Also, we added a console.log in the action controller attached to the endpoint. If we visit our browser and refresh, we'd see the following:

Screenshot 2022-04-20 at 12.33.16.png

The logs show that the code in the middleware is executed first before the code in the action controller which is also a middleware but happens to be the last middleware in the middleware. The res.send ended the request-response cycle.

Let's add more middleware and see what happens. Update the code as follows and see the logs again:

// import the express framework
const express = require('express');

// create an instance of the express app
const app = express();

// Using the middleware
app.use(logger1);
app.use(logger2);

// setup an endpoint
app.get('/', (req, res) => {
    console.log("3. Here is the action controller")
    res.send('<h1>Hello, world</h1>');
})

// logger1 Middleware
function logger1(req, res, next) {
    console.log(`1. Request URL is: ${req.url} and method: ${req.method}`);
    next();
}
// logger2 Middleware
function logger2(req, res, next) {
    console.log(`2. Request URL is: ${req.url} and method: ${req.method}`);
    next();
}

// setup server to listen on port 8000
const port = 8000;
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

In the code above, we added one more middleware function. If start the server and visit the ``` localhost:8000 ````, this is logged in the terminal:

Screenshot 2022-04-21 at 12.05.55.png

Let's do something here, let's change the order of usage of the logger and see if there is a change in our logs. Update the code as follows

// import the express framework
const express = require('express');

// create an instance of the express app
const app = express();

// Using the middleware
// Here, logger2 is used first before logger1. Remember, the order matters and we should notice a change in the console
app.use(logger2);
app.use(logger1);

// setup an endpoint
app.get('/', (req, res) => {
    console.log("3. Here is the action controller")
    res.send('<h1>Hello, world</h1>');
})

// logger1 Middleware
function logger1(req, res, next) {
    console.log(`1. Request URL is: ${req.url} and method: ${req.method}`);
    next();
}
// logger2 Middleware
function logger2(req, res, next) {
    console.log(`2. Request URL is: ${req.url} and method: ${req.method}`);
    next();
}

// setup server to listen on port 8000
const port = 8000;
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

Refresh the page and see the logs in the console:

Screenshot 2022-04-21 at 12.10.27.png

As seen in the log, the order of usage of the middleware matters, and the code in logger2 is executed before that of logger1 because of the order in which they are being used.

Install and use a third-party middleware

So far, we've created and used our custom middleware. In reality, we'd have to depend on and use some third-party middleware created by other developers. In this example, we'd install morgan, an HTTP request logger middleware for node.js. To begin, install the package using:

npm install morgan
// or if you are using yarn
yarn add morgan

In your code, require/import the package like this:

var morgan = require('morgan')

To use the middleware, add the following:

app.use(morgan('dev'))

Now, our index.js looks like this:

// import the express framework
const express = require('express');
const morgan = require('morgan');

// create an instance of the express app
const app = express();

// Using the middleware
app.use(morgan('dev'))

// setup an endpoint
app.get('/', (req, res) => {
    res.send('<h1>Hello, world</h1>');
})

// setup server to listen on port 8000
const port = 8000;
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

Here is the middleware in action. Every request made to the HTTP method is logged

Screenshot 2022-04-21 at 12.36.44.png

For a list of other popular third-party middleware with their description, check here.

Types of Middlewares

1. Global/ Application level Middlewares:

This kind of middleware is defined on a top level in the app. They apply to every single route in the app and are executed on every request made to the server. An example of this kind of middleware is the logger middleware seen earlier in our above example as shown below:

// import the express framework
const express = require('express');

// create an instance of the express app
const app = express();

// Using the middleware
app.use(logger);

// setup an endpoint
app.get('/', (req, res) => {
    console.log("Here is the action controller")
    res.send('<h1>Hello, world</h1>');
})

// logger Middleware
function logger(req, res, next) {
    console.log(`Request URL is: ${req.url} and method: ${req.method}`);
    next();
}

const port = 8000;
// setup server to listen on port 8000
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

2. Route-specific:

A route-specific middleware applies to a specific route and the middleware is only executed only when the route is visited. Their actions are scoped to the route only. To define the route-specific middleware, the middleware is bound to an instance of an express.Router(). The middleware is loaded using router.use and router.METHOD() . The first argument is the route definition while the other functions after that are the middlewares. Express allows you to pass in as much middleware as you want to the method with the last middleware being the standard express function that finally returns the response and ends the request-response cycle. An example of that is seen below:

// import the express framework
const express = require('express');
const morgan = require('morgan');
const router = express.Router();

// create an instance of the express app
const app = express();

// Using the middleware
app.use(morgan('dev'))

// setup an endpoint
app.get('/', (req, res) => {
    res.send('<h1>Hello, world</h1>');
})

// using a route specific middleware
function middleware1(req, res, next) => {
  console.log('Middleware 1 => Request URL:', req.originalUrl)
  next();
}
function middleware2(req, res, next) => {
  console.log('Middleware 2 => Request URL:', req.originalUrl)
  next();
}

router.use('/user', (middleware1, middleware2, (req, res, next) => {
  console.log('Final Middleware => Request Type:', req.method)
  next()
});

// setup server to listen on port 8000
const port = 8000;
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

3. Error handling Middleware:

This is another type of middleware. It is defined just like other types of middleware. What distinguishes it from other forms of middleware is that it takes four arguments is contrary to other forms of middleware which receive a max of 3 arguments. The middleware function has to take up to four arguments to identify as an error-handling middleware. The error-handling middleware must be placed at the end of the app to catch and process any error that occurs synchronously and asynchronously in our express application.

By default, Express has built-in error-handling middleware that gets called when there are errors in synchronous code. The error might crash the server and return the error stack which might not be user-friendly. One benefit of writing an error-handling middleware would be to handle the error more effectively and give the user a more friendly response.

An example of this is as follows:

// import the express framework
const express = require('express');
const morgan = require('morgan');
const router = express.Router();

// create an instance of the express app
const app = express();

// Using the middleware
app.use(morgan('dev'))

// setup an endpoint
app.get('/', (req, res) => {
    res.send('<h1>Hello, world</h1>');
})

// Error handling middleware
function errorHandler (err, req, res, next) {
       if (err) {
        console.log("There was an error, please try again")
   }
}

app.use(errorHandler);

// setup server to listen on port 8000
const port = 8000;
app.listen(port, () => {
    console.log(`listening on port ${port}`);
})

Further reading:

Conclusion

In this article, we discussed what middleware stands for in express, how to use it, and the different types of middleware available in express applications.

Thanks for reading!