Better way of routing in Express.js - Express Router Class

In our previous chapter, we briefly mentioned about express.Router() being a better way of implementing routing mechanism. As per the official documentation, we can use express.Router class to create a modular and mountable route handlers. A Router instance is a complete middleware and routing system; for this reason, it is often referred to as a “mini-app”. This approach provides a nice separation of concerns as routes are defined per module basis independent of each other. We will see all this definition in action. Let's start coding with a basic example of using express.Router class.

In routes/index.js file, import express package at the top of the file and then create an instance of express.Router class.

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

Once router instance is created, then we can define routes. Let's start with the index route.

   
	const sayHelloWorld = (req, res) => {
	    res.send('Hello World!')
	}
	router.get('/', sayHelloWorld);
   

If you observe code defined above with the one that we did in our earlier chapter. there is really not much difference. We just replaced app.get with router.get and rest is same.

This is it. We have our basic routing functionality ready using express.Router class. But there is one problem, the main express application doesn't know about this router object. We mentioned earlier that router instance is often called as mini-app, meaning it is a completely isolated instance from the main express application. So, we need to export this router object and then mount it on a path in main express application object.

   
	module.exports = router;
   

Let's import this router object in our main index.js file and mount it on a path using main express application object.

Import the routes/index.js file:

   
	const router =  require('./routes');
   

Then, mount the router object in a main express application object using .use() method which accepts a path and a middleware function or handler function.

   
	app.use('/', router);
   

We supplied "/" as a path to the .use() method and this basically means it will match any routes starting with / regardless of the http methods and then execute those middleware functions or handler functions. So, the above router object is executed for every request to the app. We will discuss more on middleware functions in our next chapter.

Let's say we use "/api" as a path to the .use() method. So, this will match any routes starting with /api. For ex:  app.use("/api", func) will match /api, /api/user, /api/hotel, /api/hotel/:HOTEL_ID/room and so on.

To denote that we are defining api endpoints which will be consumed by our frontend React application later on, let's use /api as a path for mounting the router object in the main express application object. Our main index.js file now looks like below:

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

	app.use('/api', router);

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

   

In our routes/index.js file, we still have other api routes defined, let's use the router instance instead of main express application object to create routes.

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


	const sayHelloWorld = (req, res) => {
	    res.send('Hello World!')
	}
	router.get('/', sayHelloWorld);
	    
	const handleLogin = (req, res) => {
	    res.json({
	        message: 'login successful'
	    })
	}
	router.post('/login', handleLogin);
	...
    ...
    ...

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

	module.exports = router;
   

Our routes/index.js file is ready. You can now start testing the endpoints using curl command:

   
   	curl localhost:3000/api/hotel
   
CURL GET request

Our application works. However, there is one problem with the above code approach. We are defining all the routes in a single file which goes against the very definition of the express.Router class which we discussed earlier. It's not modular due to which there is absolutely no separation of concerns. Let's fix this.

Before we start work on fixing above code, lets discuss something about modules. We can define module as a software component containing one or more functions which deals with a specific functionality of a software. Softwares are built using a group of independent modules that can be added or removed without having any impact on other functionalities. Let's divide our admin functionalities in various modules - user, hotel and room. This is at the high level. We can further break down these into even smaller modules. But for now, this three serves our purpose.

Inside of routes directory, create directories for user, hotel and room modules and then create index.js file inside of each directory. Our project folder structure looks like below for now.

Project directory structure

In user/index.js file, move all the routes related to the user - login, password reset etc.

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

	const handleLogin = (req, res) => {
	    res.json({
	        message: 'login successful'
	    })
	}
	router.post('/login', handleLogin);

	const requestPasswordResetLink = (req, res) => {
	    res.json({
	        message: 'password reset link sent successful'
	    })
	}
	router.post('/reset-password/request', requestPasswordResetLink);

	const changePassword = (req, res) => {
	    res.json({
	        message: 'password updated successful'
	    })
	}
	router.post('/reset-password/confirm', changePassword);

	module.exports = router;

   

In room/index.js file, move all the routes having /room  in the route.

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

	const getAllRooms = (req, res) => {
	    res.json({
	        message: 'Fetching all the rooms of a hotel'
	    })
	}
	router.get('/hotel/:hotelId/room', getAllRooms);

	const createRoom = (req, res) => {
	    res.json({
	        message: 'Creating a room of hotel'
	    })
	}
	router.post('/hotel/:hotelId/room', createRoom);

	const getRoomDetailInformation = (req, res) => {
	    res.json({
	        message: 'Fetching a detail information about room of hotel'
	    })
	}
	router.get('/hotel/:hotelId/room/:roomId', getRoomDetailInformation);

	const updateRoomInformation = (req, res) => {
	    res.json({
	        message: 'Updating room information of hotel'
	    })
	}
	router.put('/hotel/:hotelId/room/:roomId', updateRoomInformation);

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

	module.exports = router;


   

In hotel/index.js file, move removing routes starting with /hotel.

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

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


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

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

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

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

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

	module.exports = router;

   

Now that we have our modular routes ready, we need to import all these three routes in our routes/index.js file.

   
	const user = require('./user');
	const hotel = require('./hotel');
	const room = require('./room');

   

If you remember, in our main index.js file, we had app.use() to mount our router object to a path "/" to load all of our api routes. Well, router instance has also .use() method which is similar to app.use(). Let's see the syntax for router.use() as defined in the documentation.

   
	router.use([path], [function, ...] function)
   

As per official documentation, router.use() uses the specified middleware function or functions, with optional mount path, that defaults to “/”. If there are multiple middleware functions specified, then requests start at the first middleware function and if request matches with the path defined in first middleware, then the request is handled by the appropriate route handler function which then can end the request flow,  else request work their way down the middleware stack processing for each path they match.

Now, let's pass the user, hotel and room router instances as middleware functions in the router.use() method. So, in our routes/index.js file, remove all the remaining routes if any and then add router.use() method in the following way

   
	router.use("/", user, hotel, room);
   

This looks a lot better than earlier as the routes are separated per module basis. But, there is still something  that is not right even though the code works. Can you guess?

If you have guessed, in hotel/index.js, /hotel is repeated many times in the routes and also in room/index.js, /hotel/:hotelId/room is repeated in many routes, you are spot on. We really don't want to have too much repetitions. Let's fix this and make our code even more cleaner and flexible.

In hotel/index.js file, remove /hotel from all the routes.

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

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

	module.exports = router;

   

In room/index.js file, remove /hotel/:hotelId/room from all the routes.

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

	const getAllRooms = (req, res) => {
	    res.json({
	        message: 'Fetching all the rooms of a hotel'
	    })
	}
	router.get('/', getAllRooms);
	...
    ...
	const deleteRoom = (req, res) => {
	    res.json({
	        message: 'Removing room of hotel'
	    })
	}
	router.delete('/:roomId', deleteRoom);

	module.exports = router;


   

Let's modify routes/index.js file to mount the hotel and room router instances using removed path prefix.

   
	const express = require('express')
	const router = express.Router();
	const user = require('./user');
	const hotel = require('./hotel');
	const room = require('./room');

	router.use("/", user);
	router.use("/hotel", hotel);
	router.use("/hotel/:hotelId/room", room);

	module.exports = router;
   

This looks much cleaner. Still, there is some issues left. Try executing the room api routes and you will see the issue. To see the issue, try the following room api endpoint:

   
   	curl localhost:3000/api/hotel/1/room/2
   

In the terminal, from where, we run npm start command, we will see following response where in path parameter hotelId having value is not available.

   
	{ roomId: '2' }
   

By default, children router instances do not take path parameter defined in a parent router instance. To preserve the req.params values from the parent router, we need to pass { mergeParams: true } when initiating the express.Router class. So, let's modify the routes/room/index.js file to fix the issue with req.params.

   
	const express = require('express')
	const router = express.Router({ mergeParams: true });

	...
    ...
	const getRoomDetailInformation = (req, res) => {
	    console.log('#################################', req.params)
	    res.json({
	        message: 'Fetching a detail information about room of hotel'
	    })
	}
	router.get('/:roomId', getRoomDetailInformation);
    ...
    ...
   

Now, let's test one of the room apis:

   
   	curl localhost:3000/api/hotel/1/room/2
   

Now, based on the console.log we have used for the above api endpoint, we will see the following response.

   
	{ hotelId: '1', roomId: '2' }
   

This is all for routing in Express.js. Go through the code once again, you will find some duplicate code statements which can be refactored. I will leave that to you.

In our next chapter, we will discuss about middleware in detail.

Prev Chapter                                                                                          Next Chapter