CRUD operations with Express.js using In-memory storage - Travel Application

In this chapter, we will implement CRUD operation for our admin features. CRUD refers to Create, Read, Update and Delete operations. In API perspective, CRUD often corresponds to the POST, GET, PUT/PATCH and DELETE http methods. Let's look at the following table to understand more on CRUD operations.

Action HTTP Method HTTP Status codes Description
Create POST 200/201 A new resource is created. 200 - if created object is returned. 201 - if only associated id is returned.
Read GET 200 Fetches the existing resources from the data storage
Update PUT/PATCH 200/204/201 200 - if there is payload in the response. 204 - if there is no payload in the response. 201 - if new resource is created.
Delete DELETE 200/204 200 - if there is payload in the response. 204 - if there is no payload in the response.

For in-memory storage used to store data, we have couple of options - an array and a hash-table. We will be using Hash table for our application as it is faster in terms of lookups than arrays. We will be doing a lot of lookups in our application. But, Hash tables consume more memory than arrays. So, there is some tradeoff between speed and memory when choosing between hash table and arrays. Javascript Objects are the most common example of hash table. Objects are unordered collection of key-value pairs. One thing to note here is, since we are using in-memory data storage for simplicity, once server goes down, or we need to restart the server, all the existing data is erased from the memory. So, there is no persistence of data in this type of storage. And to solve this, we need to  implement various database technologies like: mysql, postgres, mongodb, redis etc to maintain the persistence of the data. We will look into it in our later chapters.

So, let's start our CRUD operations. Let's create a controllers folder and inside of it, create folders for modules - user, hotel and room.  Now our directory structure looks like below:

Project directory structure - controllers

Now, that we have our project directory ready, the first thing we need to do is move all the function definitions from routes to the respective controller modules. Let's start with user directory:

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

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

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

	module.exports = {
	    handleLogin,
	    requestPasswordResetLink,
	    changePassword
	};

   

Here, from user module, we exported handleLogin, requestPasswordResetLink and changePassword functions. From user routes, when we import this controller file, we can access these functions. Only those functions/values that are assigned to the module.exports property are accessible while importing from other files.

In routes/user/index.js file, import the user controller and update the route handler references:

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

	router.post('/login', user.handleLogin);
	router.post('/reset-password/request', user.requestPasswordResetLink);
	router.post('/reset-password/confirm', user.changePassword);

	module.exports = router;
   

As you can see, user route file is much cleaner and readable, only handles routing functionality with associated module controller function. All the module function logic is now handled by controller functions.

Also, move all the function definitions from routes/hotel/index.js to the hotel controller module. Also add one additional function called internalHelperFunc which we will not assign to the module.exports property. Now when hotel controller file is imported, internalHelperFunc function will not be available to use from where it is imported.

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

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

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

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

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

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

	const internalHelperFunc = (req, res) => {
	    res.json({
	        message: 'Sensitive info'
	    })
	}

	module.exports = {
	    listAllHotels,
	    getHotelDetailInformation,
	    createHotelInformation,
	    updateHotelInformation,
	    publishHotelInformation,
	    removeHotelInformation
	};

   

In routes/hotel/index.js file, import the hotel controller and update the route handler references:

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

	/// public endpoints
	router.get('/', hotel.listAllHotels);
	router.get('/:hotelId', hotel.getHotelDetailInformation);

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

	router.post('/', hotel.createHotelInformation);
	router.put('/:hotelId', hotel.updateHotelInformation);
	router.patch('/:hotelId', hotel.publishHotelInformation);
	router.delete('/:hotelId', hotel.removeHotelInformation);

	module.exports = router;
   

Try accessing internalHelperFunc from routes/hotel/index.js file, You will not find that function in the hotel object and when you try to access it, it will throw an error.

module.exports function visibility feature

Now repeat the same process for the room module as well. This will be a practice lesson for you.

Now, let's start the CRUD operation with hotel module using Javascript Object as data storage. We have chosen this memory storage to show you how it will impact the scaling part when we deploy it to the cloud and many users start to use it. We will see this in our later chapters.

To collect payload from the request object, we need to inject couple of built-in middleware in index.js located at root of the directory.

   
	app.use(express.json());
	app.use(express.urlencoded({ extended: false }));
   

express.json() is a built-in middleware to parse incoming requests with JSON payloads and is based on body-parser package. With the latest versions of express.js, we don't need to separately install the body-parser package.

express.urlencoded({ extended: false }) is a built-in middleware to parse incoming requests with urlencoded payloads and is also based on body-parser package. After implementing these middleware, we can access payload from request object using req.body property.

We can submit JSON payload using curl in following way:

   
   	curl --location --request POST 'http://localhost:3000/api/hotel' \
	--header 'Authorization: supersecret' \
	--header 'Content-Type: application/json' \
	--data-raw '{
	    "name": "everest"
	}'
   

We can submit urlencoded payload using curl in following way:

   
   	curl --location --request POST 'http://localhost:3000/api/hotel' \
	--header 'Authorization: supersecret' \
	--header 'Content-Type: application/x-www-form-urlencoded' \
	--data-urlencode 'name=everest'
   

Our index.js from root directory now looks like below:

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

	app.use(express.json());
	app.use(express.urlencoded({ extended: true }));

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

   

In our controllers/hotel/index.js file, let's define a variable of type Object at the top of the file.

   
	let hotels = {};
   

Let's define a few fields for hotel data storage.

Fields Type
id string
name string
description string
amenities string
is_published bool
guest_capacity number

Let's start with create hotel feature.

For id field, we can either use auto-incremented integer value or a uuid - (universally unique identifier) value. It's a value that uniquely identifies something and there is almost no chance of a value being repeated twice.

For uuid, we will use a third-party npm package called uuid.

   
   	npm i uuid
   

Import the uuid package at the top of the file:

   
	const { v4: uuidv4 } = require('uuid');
   

Here, we are importing v4 method from uuid package and then renaming it as uuidv4 for better readability.

Now our create hotel information function looks like below:

   
	const createHotelInformation = (req, res) => {
	    try {
	        const hotelId = uuidv4();
	        const hotelObj = {
	            id: hotelId,
	            name: req.body.name,
	            description: req.body.description,
	            amenities: req.body.amenities,
	            is_published: false,
	            guest_capacity: req.body.guest_capacity
	        };

	        req.logger.debug('Creating hotel with info ', hotelObj);
	        hotels[hotelId] = hotelObj;
	        req.logger.debug('Hotel created successfully ');
	        res
	        .status(200)
	        .json({
	            data: hotelObj
	        })
	    } catch (err) {
	        req.logger.error('Error creating hotel info ', err);
	        throw err;
	    }
	}
   

Here, we are doing following things in the above function:

  • We are using uuid npm package for id value. To get the value from uuid package, we need to call the imported function which in our case is uuidv4()
  • Then we defined a hotel Object with different data fields needed to create a hotel information data. We access the payload submitted via api in request object using req.body property
  • Field is_published is set to false as this will be set only after all the hotel and room info is ready.
  • Then, we are logging the hotel object containing all the user submitted information, so that, we can check the payload if needed in future as well. We used req.logger property for logging. If you remember in our previous chapter, we defined the logger middleware and modified the request object to add logger object.
  • Use key-value pair for creating hotel and in our case key being the id and value being the hotel object. To set the key-value pair in an object, we can use Object[key] = value syntax and To get the value of a particular key, we can use data = Object[key] syntax.
  • Log the success message. These logs messages are very much necessary to debug when issue arises.
  • Send the 200 response with the hotel object.
  • If error occurs, then we are logging error infomation as well. Then we throw the error, so that, global error handler which we defined in our root index.js file will handle it.

Test the hotel create api endpoint with curl command:

   
   	curl --location --request POST 'http://localhost:3000/api/hotel' \
	--header 'Authorization: supersecret' \
	--header 'Content-Type: application/json' \
	--data-raw '{
	    "name": "Hyatt Regency",
	    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
	    "amenities": "Free Wifi, Free drinks",
	    "guest_capacity": 2000
	}'
   

Once hotel is created successfully, check hotels variable, it will look like below:

   
	{
	  '401766ce-1ce2-4ce6-b1be-a922d56f5a14': {
	    id: '401766ce-1ce2-4ce6-b1be-a922d56f5a14',
	    name: 'Hyatt Regency',
	    description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
	    amenities: 'Free Wifi, Free drinks',
	    is_published: false,
	    guest_capacity: 2000
	  }
	}
   

Let's implement list all hotels function:

   
	const listAllHotels = (req, res) => {
	    try {
	        req.logger.debug('Fetching all the hotel information');
	        const lstHotels = Object.values(hotels)
	        res
	            .status(200)
	            .json({
	                data: lstHotels
	            })
	    } catch (err) {
	        req.logger.error('Error fetching hotels ', err);
	        throw err;
	    }
	}
   

Here, We used Object.values(hotels) to get the list of hotels from an object. Object.values() accepts an object and then returns its own enumerable property values.

Let's fetch the list of hotels:

   
   	curl --location --request GET 'http://localhost:3000/api/hotel'
   

We will get the following response:

   
	{
	    "data": [
	        {
	            "id": "401766ce-1ce2-4ce6-b1be-a922d56f5a14",
	            "name": "Hyatt Regency",
	            "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
	            "amenities": "Free Wifi, Free drinks",
	            "is_published": false,
	            "guest_capacity": 2000
	        }
	    ]
	}
   

Let's implement get hotel detail information feature. Since we have the hotelId sent in the path parameter, we can get the hotel detail info using object[key] syntax. Here, req.params.hotelId is the key value and object is hotels variable. This approach is very easy and highly efficient as well.

   
	const getHotelDetailInformation = (req, res) => {
	    try {
	        const hotelId = req.params.hotelId;
	        req.logger.debug('Fetching detailed information about the hotel for id ', hotelId);
	        const hotelInfo = hotels[hotelId];
	        res
	            .status(200)
	            .json({
	                data: hotelInfo
	            })
	    } catch (err) {
	        req.logger.error(`Error fetching hotel detail info with id ${hotelId}`, err);
	        throw err;
	    }
	}
   

Fetch the hotel detail info using curl command - replace HOTEL_ID with the actual uuid of the hotel.

   
   	curl --location --request GET 'http://localhost:3000/api/hotel/HOTEL_ID'
   

Let's implement update hotel feature as well.

   
	const updateHotelInformation = (req, res) => {
	    try {
	        const hotelId = req.params.hotelId;
	        const hotelInfo = hotels[hotelId];
	        const hotelObj = {
	            name: req.body.name,
	            description: req.body.description,
	            amenities: req.body.amenities,
	            guest_capacity: req.body.guest_capacity
	        };

	        req.logger.debug('Updating hotel with info ', hotelObj);
	        hotels[hotelId] = {
	            ...hotelInfo,
	            ...hotelObj
	        };
	        req.logger.debug('Hotel updated successfully ');
	        res
	        .status(200)
	        .json({
	            data: {
	                ...hotelInfo,
	                ...hotelObj
	            }
	        })
	    } catch (err) {
	        req.logger.error(`Error updating hotel info  with id ${hotelId}`, err);
	        throw err;
	    }
	}
   

Here, we are doing following things to update existing hotel info.

  • We get the hotelId using the path parameter defined in the route.
  • We get the existing hotel info by using Object[key] syntax as discussed in get detail api section.
  • We create hotel object containing changed hotel data.
  • We used javascript spread operator to combine existing unchanged data with changed object and then replace the existing one with the newly created object.

Use following CURL command to test update api - replace HOTEL_ID with the actual id:

   
   	curl --location --request PUT 'http://localhost:3000/api/hotel/HOTEL_ID' \
	--header 'Authorization: supersecret' \
	--header 'Content-Type: application/json' \
	--data-raw '{
	    "name": "Hyatt Regency - Kathmandu",
	    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
	    "amenities": "Free Wifi, Free drinks, Free Airport Pickup",
	    "guest_capacity": 180
	}'
   

After successful update operation, If you fetch the detail information for hotel with replaced HOTEL_ID, it will look something like below:

   
	{
	    "data": {
	        "id": "401766ce-1ce2-4ce6-b1be-a922d56f5a14",
	        "name": "Hyatt Regency - Kathmandu",
	        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
	        "amenities": "Free Wifi, Free drinks, Free Airport Pickup",
	        "is_published": false,
	        "guest_capacity": 180
	    }
	}
   

Let's implement publish hotel feature:

   
	const publishHotelInformation = (req, res) => {
	    try {
	        const hotelId = req.params.hotelId;
	        const hotelInfo = hotels[hotelId];

	        req.logger.debug('Publishing hotel info for id ', hotelId);
	        hotels[hotelId] = {
	            ...hotelInfo,
	            is_published: true
	        };
	        req.logger.debug('Hotel published successfully ');
	        res
	        .status(204).send();
	    } catch (err) {
	        req.logger.error(`Error publishing hotel info with id ${hotelId}`, err);
	        throw err;
	    }
	}
   

Use the following CURL command to publish the hotel info - replace HOTEL_ID with the actual id:

   
   	curl --location --request PATCH 'http://localhost:3000/api/hotel/HOTEL_ID' \
	--header 'Authorization: supersecret'
   

Let's implement delete hotel feature as well:

   
	const removeHotelInformation = (req, res) => {
	    try {
	        const hotelId = req.params.hotelId;
	        req.logger.debug('Deleting hotel info with id ', hotelId);
	        delete hotels[hotelId];
	        req.logger.debug('Hotel deleted successfully ');
	        res
	        .status(204).send();
	    } catch (err) {
	        req.logger.error(`Error deleting hotel info with id ${hotelId}`, err);
	        throw err;
	    }
	}
   

To remove a property from an object, we need to use the delete operator. In our case, property is the id value and object is the hotels variable.

Use the following CURL command to delete the hotel info - replace HOTEL_ID with the actual id:

   
   	curl --location --request DELETE 'http://localhost:3000/api/hotel/HOTEL_ID' \
	--header 'Authorization: supersecret'
   

After successful delete operation, try fetching the list of hotels and we will get empty array. For room module, implement the functionality in the same way as we did just now for hotel module.

In our next chapter, we will discuss about various tools needed to speed up our development process.

Prev Chapter                                                                                          Next Chapter