Routing in Express.js - add travel application routes

In basic terms, routing is a mechanism in which HTTP requests made by the clients are routed to the appropriate code handler. Express.js provides a robust routing mechanism.  Routes/endpoints are defined using the combination of HTTP methods and a path pattern. Each route can have one or more handler functions which are executed when the client request matches both the HTTP method as well as the path. We already had a basic route defined in our earlier chapter. Let's revise that:

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

We defined an index route for http GET method. This follows the following syntax:

   
   	 app.METHOD(PATH, HANDLER)
   

where:

  • app is an instance of an express application
  • METHOD is the HTTP methods like GET, POST, PUT, PATCH, DELETE etc in lowercase
  • Path is a url string that relays an intent of the action which will be done in the backend
  • HANDLER is the function which gets executed when the method and the path matches for the client request

Hypertext Transfer Protocol (HTTP) is a protocol that defines the standards for transferring the resources between server and client. Resources can be files (html, css, js, images), data etc. HTTP defines a set of request methods which indicates the desired action to be performed for a given resource in the server. Some of the commonly used methods are as follows:

GET
Used only for reading/fetching resources. For successful GET request, we will get the requested information from the server. GET requests are cacheable and idempotent. It doesn't change the state of the server. Cacheable means storing server response in the client itself for faster deliveries. Idempotent means making identical requests to a resource, no matter how many times, it will always have the same effect.

POST
Used for creating new resources. A client submits information as a payload based on which a new resource is created on the server.

PUT
Used for modifying the existing resource. If the resource do not exists, can also create a new resource.

PATCH
Used for modifying only some part of the resource.

DELETE
Used for deleting the resource.

Now that we are aware of the commonly used HTTP methods, Let's start creating routes for admin section. Table below shows the different routes that we need to create for admin features.

Routes Purpose
POST /login Login using username and password combination
POST /reset-password/request Request for password reset link in the email
POST /reset-password/confirm Change password using password reset link sent in the email
GET /hotel Get list of hotels
POST /hotel Create a hotel information
GET /hotel/HOTEL_ID Get detail information about the particular hotel by HOTEL_ID
PUT /hotel/HOTEL_ID Update the existing information of the hotel
PATCH /hotel/HOTEL_ID/publish Publish hotel information
DELETE /hotel/HOTEL_ID Delete hotel information
GET /hotel/HOTEL_ID/room/ Get all the rooms associated with the hotel
POST /hotel/HOTEL_ID/room/ Create rooms associated with the hotel
GET /hotel/HOTEL_ID/room/ROOM_ID Get detail information about the room of a hotel
PUT /hotel/HOTEL_ID/room/ROOM_ID Update the room details
DELETE /hotel/HOTEL_ID/room/ROOM_ID Delete the room information

Let's start coding now:

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

Let's modify above code by separating the route handler function for better readability.

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

	app.post('/login', handleLogin);
   

Here we are sending json response to the client using json method from response object. Let's now add all other routes defined in the table:

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

	app.post('/login', handleLogin);

	const requestPasswordResetLink = (req, res) => {
	    res.json({
	        message: 'password reset link sent successful'
	    })
	}


	app.post('/reset-password/request', requestPasswordResetLink);

	const changePassword = (req, res) => {
	    res.json({
	        message: 'password updated successful'
	    })
	}

	app.post('/reset-password/confirm', changePassword);


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

	app.get('/hotel', listAllHotels);

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

	app.post('/hotel', createHotelInformation);
   

All the above routes are static ones. For declaring rest of the routes, we need to know a little bit about dynamic routes. One of the ways to create dynamic routes is using path parameters. Path parameters is a kind of parameter which is attached to the URL endpoint itself and is separated by "/" character. They are usually used to identify a specific resource. Once path parameter is defined in a route, it cannot be skipped as skipping it would change the route entirely and can result in 404 not found error or other unintended results. Let's look at how we will define path parameter in a route.

   
	/user/:userId
   

Here, We attach /:userId to the url endpoint and in Node.js, this is how we define a path parameter. :userId is the dynamic variable value and must be replaced with the actual id of the user. Ex:

   
	/user/1
   

As you can see, we replaced :userId with 1. Here 1 is the unique id of the user. For different users, it will be different. We can also have multiple path parameters in a url endpoint . Ex:

   
	/user/:userId/child/:childId
   

Here, :userId and :childId are the path parameters attached to the url endpoint and must be replaced with the id of the user for :userId and id of the children for :childId in the same exact order.

   
	/user/1/child/2
   

Here, we replaced :userId with 1 and :childId with 2. If we interchange that values, it would mean totally different user and the child. In the route handler function, we can access these path parameters using req.params.PARAMETER_NAME

Ex:

   
	req.params.userId
	>> 1

	req.params.childId
	>> 2
   

Now that we know how to define dynamic routes using path parameter, Let's define rest of the application routes.

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

	app.get('/hotel/:hotelId', getHotelDetailInformation);

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

	app.put('/hotel/:hotelId', updateHotelInformation);

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

	app.patch('/hotel/:hotelId', publishHotelInformation);

	const removeHotelInformation = (req, res) => {
	    res.json({
	        message: 'Removing hotel'
	    })
	}

	app.delete('/hotel/:hotelId', removeHotelInformation);


	const getAllRooms = (req, res) => {
	    res.json({
	        message: 'Fetching all the rooms of a hotel'
	    })
	}

	app.get('/hotel/:hotelId/room', getAllRooms);

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

	app.post('/hotel/:hotelId/room', createRoom);

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

	app.get('/hotel/:hotelId/room/:roomId', getRoomDetailInformation);

	const updateRoomInformation = (req, res) => {
	    res.json({
	        message: 'Updating room information of hotel'
	    })
	}

	app.put('/hotel/:hotelId/room/:roomId', updateRoomInformation);

	const deleteRoom = (req, res) => {
	    res.json({
	        message: 'Removing room of hotel'
	    })
	}

	app.delete('/hotel/:hotelId/room/:roomId', deleteRoom);


   

Kudos to us, we have defined all of the routes for admin features. But carefully looking at our main index.js file, you would notice, even with very basic routes implementation, our index.js file is getting longer and we have to frequently scroll  up and down. All the routes are defined in a single place with no separation of concerns between user functionality, hotel functionality and room functionality. This is a much bigger problem when dealing with large and complex applications as it would make maintenance of the application much harder. Another thing that we can do is just move all the routes to a separate file  and import that file in index.js file. That would make index.js file clean but then shifts the problem to another file. Let's see that in action:

Let's create a folder called routes and inside of that folder, create a file called index.js file.

   
   	mkdir routes
   	cd routes/
   	touch index.js
   

Move all the routes and associated functions to routes/index.js file.

   
	module.exports = (app) => {

	    const sayHelloWorld = (req, res) => {
	        res.send('Hello World!')
	    }
	    app.get('/', sayHelloWorld);
	    
	    
	    const handleLogin = (req, res) => {
	        res.json({
	            message: 'login successful'
	        })
	    }
	    app.post('/login', handleLogin);
        
        ...
        ...
        ...
	    
	    const deleteRoom = (req, res) => {
	        res.json({
	            message: 'Removing room of hotel'
	        })
	    }
	    app.delete('/hotel/:hotelId/room/:roomId', deleteRoom);
	}
   

Import the routes/index.js file in our main index.js file.

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

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

   

We use following code statement to import a separate route file in our main index.js file.

   
	require('./routes')(app);

   

You might wonder why there is no index.js file being mentioned in the require function. Well, index.js file is used by default when we require a folder. If index.js file do not exists, it throws an error. And, we are passing an instance of an express application to the routes file which will be used to define routes. Any other name of the file, you must also specify the filename along with the entire path while using require.

As you can see, this looks a little better than earlier but it still has problems. With complex application, there would be hundreds of endpoints and dealing the routing the way we did, it will be very hard to maintain as we cannot modularize the routing. Everything is defined on the instance of main express application and there lacks separation of concerns between different functionalities.

There is a much better way to handle routing in express.js using express.Router(). We will look into it in detail in next chapter.

Prev Chapter                                                                                          Next Chapter