Middleware in Express.js

A middleware is a type of function which can be used to intercept the application's request-response mechanism and has access to the request object, response object and the next function. Next function can be another middleware or a handler function that ends the request. It is one of the most important concepts in express.js and knowingly or unknowingly, we have already used middleware in our earlier chapters.

Some of the things that we can do using middleware are:

  • Modify the request and/or response objects
  • End the current request/response cycle
  • Call next middleware in the stack
  • Perform various checks before the control flow goes to the main function

Let's get into the action. We have been working with the admin functionalities till now. When we say admin functionalities, some of the functionalities that we have defined should only be accessible to the admin users. All the other type of users should be denied access. Let's first define a middleware function which checks the authentication of the requests and see if the user passes authentication requirements. For now, we will follow a very simple approach using a HTTP header with some value. For this kind of checks, we use JWT tokens which we will look into detail later when we work on the user module.

Create a folder called middlewares in the root of the project directory. Now our folder structure looks like below:

Middlewares folder structure

Inside of our middlewares/index.js file, lets define a function which performs authentication checks to determine, if the routes can be accessible or not.

   
	const authenticateUsers = (req, res, next) => {
	    if (req.headers['authorization'] && req.headers['authorization'] === 'supersecret') {
	        return next();
	    }
	    res
	    .status(401)
	    .json({
	        message: 'Authentication Failed'
	    });
	  };

	module.exports = authenticateUsers;
   

In express.js, we can access the header values using headers property from the request object.

   
	req.headers
   

In the above function, first we checked if there is a field called authorization in the header and if the filed exists then, compare the value of the authorization header with value "supersecret". If value matches, then we pass the control flow to the next middleware or if next middleware do not exists, then the other handler functions. If value do not match, then we throw unauthorized response containing authentication failed message. 401 is the http status code used for invalid authentication credentials.

Now that we have the middleware ready, let's inject that middleware in the routes that should only be accessible to the admin users. Let's look into all the hotel routes and determine the authentication requirements.

Routes Auth Required Remarks
GET /hotel No We want to show list of hotels even without login
POST /hotel Yes
GET /hotel/HOTEL_ID No We want to show hotel detail info without login
PUT /hotel/HOTEL_ID Yes
PATCH /hotel/HOTEL_ID/publish Yes
DELETE /hotel/HOTEL_ID Yes

In routes/hotel/index.js file, let's import the middleware and then inject the middleware function in the routes.

   
	const authenticateUsers = require('../../middlewares');
   

We can inject authenticateUsers middleware function in the route in the following way:

   
	router.post('/', authenticateUsers, createHotelInformation);
   

So, we inject the middleware function in between  a path and a handler function. If user is authenticated, then a handler function is executed else the request ends with 401 error message. Let's try the curl command:

   
   	curl -H "Authorization: invalidsecret" \
	-d '{ "name": "hotel" }' \
   	-X POST localhost:3000/api/hotel
   

Here, we passed invalid value for Authorization header and we should be getting 401 error.

CURL POST with Header values

Now, pass the valid value which is supersecret in the header and try again:

   
   	curl -H "Authorization: supersecret" \
	-d '{ "name": "hotel" }' \
   	-X POST localhost:3000/api/hotel
   

Now that authorization header value matches with the one that we are checking in the middleware function definition, we will get the hotel create message.

CURL command with valid auth creds POST

So, we have implemented middleware function and test verified it for a specific route. These types of middleware function are called router level middleware as it is bound to an instance of express.Router class. What if all of the routes in hotel module require authentication checks? We don't want to include the middleware function in every routes, that will be tedious and also not a best practice. What we need to do is before defining any routes, we need to inject the authentication check middleware using router.use() method which we discussed in our previous chapter. Let's implement that in code.

Note: Remove the  authenticateUsers middleware function from the post method for creating hotels.

   
	const express = require('express')
	const router = express.Router();
	const authenticateUsers = require('../../middlewares');

	router.use(authenticateUsers)

	const listAllHotels = (req, res) => {
	    res.json({
	        message: 'Fetching all hotels'
	    })
	}
	router.get('/', listAllHotels);

	...
    ...

	module.exports = router;

   

So, we have injected authentication check middleware using router.use() method before defining any routes. The first thing that happens here is - requests that matches with /hotel is intercepted by this middleware and then check for authentication headers. If the authentication passes, then only the request will flow to the route endpoints and the associated route handlers. You might wonder why there is no path defined in the router.use. In all our previous chapters, we have used path along with the handler functions in router.use() method. Well, if you remember in our previous chapter, we mentioned, if we don't specify any path, it will default to "/" meaning it will match any requests starting from /hotel in our case.

As per our requirement, we don't need authentication checks in our get apis - api to get hotel list and api to get hotel detail information. You might wonder what do we do next? - Do we inject the middleware functions on each routes?

Well, to solve this, we need to now group public routes and private admin only routes. After grouping, keep the public endpoints at the top of the file and after all the public endpoints are defined and before private endpoints are defined, we need to inject the authentication check middleware using the router.use() method as above. Let's do that:

   
	const express = require('express')
	const router = express.Router();
	const authenticateUsers = require('../../middlewares');

	/// public endpoints
	const listAllHotels = (req, res) => {
	    res.json({
	        message: 'Fetching all hotels'
	    })
	}
	router.get('/', listAllHotels);

	const getHotelDetailInformation = (req, res) => {
	    console.log(">>>", req.params)
	    res.json({
	        message: 'Fetching detail information about hotel'
	    })
	}
	router.get('/:hotelId', getHotelDetailInformation);

	/// private endpoints
	router.use(authenticateUsers);

	const createHotelInformation = (req, res) => {
	    res.json({
	        message: 'Hotel created'
	    })
	}
	router.post('/', createHotelInformation);

	const updateHotelInformation = (req, res) => {
	    res.json({
	        message: 'Updating information about hotel'
	    })
	}
	router.put('/:hotelId', updateHotelInformation);

	const publishHotelInformation = (req, res) => {
	    res.json({
	        message: 'Publishing information about hotel'
	    })
	}
	router.patch('/:hotelId', publishHotelInformation);

	const removeHotelInformation = (req, res) => {
	    res.json({
	        message: 'Removing hotel'
	    })
	}
	router.delete('/:hotelId', removeHotelInformation);

	module.exports = router;


   

Now, all the routes defined after router.use() with authentication check middleware will require valid authorization header value, else the response will be 401 error. Well, this is our final code solution. You can repeat the same process in room module as well.

Let's discuss about another type of middleware called application level middleware which is bound to the main express application object. There are many use cases in which we can use application level middleware and among those many, one of the most important use case is logging feature. With logging, we can view the overall events happening with the system using various tools. Without proper logging, supporting and maintaining the production applications is almost impossible. There are various libraries that we can use to log events. Some of the most popular ones are winston and pino. We will use winston package for logging  purpose. Install the package:

   
   	npm install winston
   

Create utils directory in the root of the project directory. Inside of utils directory, create logger.js file. Now our project directory structure looks like below:

Project directory structure

In utils/logger.js file, write the following code:

   
	const winston = require('winston');

	const logger = winston.createLogger({
	  level: 'debug',
	  format: winston.format.json(),
	  defaultMeta: { service: 'travel-app' },
	  transports: [
	  ],
	});

	if (process.env.NODE_ENV !== 'production') {
	  logger.add(new winston.transports.Console({
	    format: winston.format.simple(),
	  }));
	}

	module.exports = logger;
   

I would not get into detail in logging in this chapter. We will have a separate chapter covering logging only. The above piece of code logs the messages in the console/terminal from which the application is started.

In index.js file from root directory, import the utils/logger.js file

   
	const logger =  require('./utils/logger');

	app.use((req, res, next) => {
	    req.logger = Object.freeze(logger);
	    next();
	});
   

Here, after importing the logger file from utils directory, we injected the application level middleware using app.use() method and in that middleware function, we modified the request object to add the logger details. Now from any modules within this express application, we can access logger object using req.logger and then start logging messages.

Then, there are built-in middleware and some are listed below:

   
	express.json([options]) -> Parses incoming requests with JSON payloads
	express.static(root, [options]) -> Serves static assets
	express.urlencoded([options]) -> Parses incoming requests with URL-encoded payloads
   

Also, there are third-party middlewares that we can use in our application. Some of them are listed below:

  • body-parser

    Body parsing middleware. Parse incoming request bodies in a middleware before the handlers, available under the req.body property.

  • compression

    Compression middleware to compress the response to reduce size

  • morgan

    HTTP request logger middleware for node.js

  • cors

    Enable cross-origin resource sharing (CORS) with various options.

  • multer

    For handling multipart/form-data, which is primarily used for uploading files.

Finally we have error handling middleware which is different than other type of middleware. It always takes four arguments and first argument is always error object. Rest of the other arguments are request, response and next as with other type of middleware. Note that it must be four arguments to identify the middleware as the error handling middleware. Otherwise, it will fail to handle the error.

In our index.js file in the root directory, let's implement the error handling middleware so that any uncaught errors in the modules will be handled here.

   
	const express = require('express');
	const app = express();
	const router =  require('./routes');
	const logger =  require('./utils/logger');

	app.use((req, res, next) => {
	    req.logger = Object.freeze(logger);
	    next();
	});
	app.use('/api', router);

	app.use((err, req, res, next) => {
	    res
	    .status(500)
	    .json({ message: 'Internal server error' });
	})

	const port = 3000;
	app.listen(port, () => {
	    console.log(`Travel application listening on port ${port}`)
	});

   

This covers middleware concept. In our next chapter, Let's do CRUD operations for hotel and room modules using In-memory storage.

Prev Chapter                                                                                          Next Chapter