Accessing Azure CosmosDB database using Node.js and Express REST APIs

Last week, I was conducting a session on MEAN Stack. In this training, I taught my students how to access MongoDB in an Express application.

One of the attendees asked me to create a demo on accessing CosmosDB using Node.js apps.

In this tutorial, I will show you the steps to do the same.

CosmosDB is a multi-model, globally distributed database service on Microsoft Azure. It provides NoSQL data storage using Table Storage, SQL API, MongoDB, Gremlin, etc. More information on this can be read here https://docs.microsoft.com/en-us/azure/cosmos-db/introduction. I have also written a post on using Azure CosmosDB with ASP.NET Core and Angular. You can read about it over here https://www.dotnetcurry.com/windows-azure/1395/cosmosdb-webapi-angular-client.

In modern web application development  based on Isomorphic apps (pure JavaScript from Client-to-server e.g. MEAN/MERN Stack), the data storage is preferred as NoSQL data store. On  top of that, if this data store is provided and managed by and on cloud, then such storage services are highly preferred.


CosmosDB is one such service provided on Microsoft Azure. In this tutorial, we will go through all the steps for accessing SQL API of CosmosDB using Node.js application. The application is implemented as shown in the following image



Image 1: Accessing CosmosDB using Node.js application.

We will implement a Node.js application using TypeScript. To implement the application, we need Microsoft Azure Cosmos JavaScript SDK. This is the library for SQL API of Azure CosmosDB Service. This can be installed using the following commands:

npm install --save @azure/cosmos


We need to create an Azure CosmosDB Account to create database and container. Please visit this link to read steps for creating Azure CosmosDB Account.

Once the Account is created, navigate to the newly created account and from the property pan, click on the Keys. This will show the URI and PRIMARY KEY. Copy this URI and PRIMARY KEY. 

We need this to authenticate the Node.js application to access CosmosDB to perform operations like create database and container and perform CRUD operations. The following image shows URI and PRIMARY KEY.


 Image 2: The URI and PRIMARY KEY for access the CosmosDB 

To develop the application, we will Visual Studio Code (VSCode), a free IDE.

Step 1: Create a folder of the name nodecosmos  on the drive and open this folder in VSCode. This folder will become workspace for the application.

Step 2: Open the command prompt and navigate to the nodecosmos folder. From the command prompt run the following command:

npm init -y

This will generate package.json file in the folder. In the package.json, in its dependencies section, mention the following packages. We will need these packages for writing code:

"@azure/cosmos": "^2.1.5",
        "@types/express": "^4.17.0",
        "@types/node": "^12.0.8",
        "async": "^3.0.1",
        "body-parser": "^1.19.0",
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "ts-node-dev": "^1.0.0-pre.40",
        "typescript": "^3.5.1"

Step 3: Run the npm install command from the command prompt. This will install all the packages mentioned in dependencies. 

The ts-node-dev package is a TypeScript execution package for Node.js. This will continuously execute the server side typescript code even during the code changes /updates. Modify the scripts section to set command to use ts-node-package for executing server-side as as shown in the following listing:
  
"scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev": "ts-node-dev --respawn --transpileOnly ./server.ts"
    }

Listing 1: The scripts code to run the typescript file on server side.

Step 4: In the project add three folders and name them as appconfig, models and controllers. In the appconfig folder, add a new file and name it as config.ts. In this file, add the following code:


export const appConfig = {
    uri: '',
    authKey: '',
    databaseId: 'Seller',
    containerId:'ProductsCatelog'
};

Listing 2: The Data Access configuration values copied from the Keys section of the CosmosDB property pane from Azure portal

The above object contains the URI and the PRIMARY Key for the CosmosDB Account copied from the Azure portal. This is needed to access CosmosDB service and performing CRUD operations. We are setting the databaseId name as Seller and containerId name as ProductsCatalog.

Step 4: In the models folder, add a new typescript file and name it as productdetails.ts. This file will contain ProductDetails class. This class will contain public members that will be used to create data (or records) in CosmosDB SQL API container. The following listing contains the class code:

export class ProductDetails {
    constructor(
        public ProductId: number,
        public ProductName: string,
        public CategoryName: string,
        public SubCategory: string,
        public Description: string,
        public Manufacturer: string,
        public Price: number
    ){}
}

Listing 3: The ProductDetails class

Step 5: In the models folder, add a new file and name it as dao.ts. We will write code to perform operations with CosmosDB in this file. Add the following code in this file:

// 1. importing the required APIs for performing operations with Cosmos
import { CosmosClient, Database, Container } from '@azure/cosmos';
// 2. importing the application configuration constant for URL and Primary key 
import { appConfig } from "./../appconfig/config";
// 3. importing product details class
import { ProductDetails } from './productsdetails';
// 4. The Data Access class to create various operations
export class Dao {
    // 4.1. defining class level members 
    private client: CosmosClient;
    private databaseId: string;
    private collectionId: string;
    private database: Database;
    private container: Container;
    // 4.2. constructor contains code to connect to CosmosDB Account
    constructor() {
        // 4.3. creating an instance of the CosmosClient and settings keys
        this.client = new CosmosClient({
            endpoint: appConfig.uri,
            auth: {
                masterKey: appConfig.authKey
            }
        });
        // ends here
        // 4.4. set the databaseId and containerId
        this.databaseId = appConfig.databaseId;
        this.collectionId = appConfig.containerId;
        //ends here
    }

    // 4.5. initialize the CosmosDB connections to create database and container
     async initDbAndContainer() {
         // create database if not exist
         const responseDb = await this.client.databases.createIfNotExists(
                                 {id:this.databaseId});
         this.database = responseDb.database;   
         console.log(`Database Created ${this.database}`);
         // ends here

         // create a container if not exist
          const responseContainer = await this.database.containers.createIfNotExists({
            id:this.collectionId
          });  
          this.container =  responseContainer.container;
          console.log(`Container Created ${this.container}`);
         // ends here
     }
    // ends here

    // 4.6. add product
    async addProduct(product:ProductDetails) {
        const resp = await this.container.items.create(product);
        console.log(`In the addProduct ${JSON.stringify(resp.body)}`);
        return resp.body;
    }
    // ends here

    // 4.7. query to data
    async queryData() {
      const query = 'SELECT * FROM root';
      if(!this.container){
          throw new Error('The specified collection is not present');
      }  
      const result = await this.container.items.query(query).toArray();
      return result.result;
    }
    // ends here
    // 4.8. code to update product
    async updateProduct(id:string,product:ProductDetails){
        // get the document base on id parameter
        const record =  await this.container.item(id).read();
        console.log(`Record for update ${JSON.stringify(record.body)}`);
        // set the updated values
        record.body.ProductName = product.ProductName;
        record.body.CategoryName = product.CategoryName;
        record.body.SubCategory = product.SubCategory;
        record.body.Manufacturer = product.Manufacturer;
        record.body.Description = product.Description;
        record.body.Price = product.Price;
        console.log(`Record for update values ${JSON.stringify(record.body)}`);

        const updated = await this.container.item(id).replace(record.body);
        console.log(`Record after update ${JSON.stringify(updated.body)}`);
        return updated.body;
        
    }
}

Listing 4: Code to perform operations like create database, container, items, etc. 

The above code has the following specifications (Note: The following line numbers matches with the code comment numbers given in the above code.)

  1. This imports the required APIs for performing operations with CosmosDB. The CosmosClient provides a client-side logical representation of the Azure CosmosDB database account. This is used to configure and execute requests in the Azure CosmosDB service. The Database object is used for reading and deleting an existing database. The Container object is used to perform operations like reading, updating, deleting container by id.
  2. This step imports the appconfig object. We have created this object in Step 4 for defining URI, Primary Key,  Database and Container. We will be using this object in the current code for authenticating against the Azure CosmosDB account.
  3. This imports ProductDetails class 
  4. The Dao class is used for performing operations as explained in following points
    1.  This defines class level private members for CosmosClient, DatabaseId, CollectonId, database and container name.
    2. The constructor is defined to initialize the environment for CosmosDB service.
    3. This step is used to create an instance of CosmosDB client using URI and PRIMARY key.
    4. Here the default names for Database and Container is set. We are using database name as Seller and Container name as ProductCatelog
    5.   The initDbAnContainer() method will contain code for creating Database and Container if they are not already exists. This is an asynchronous method. 
    6. The addProduct() is an asynchronous method that accepts the ProductDetails object and create a new item into the container.
    7. The queryData() is an asynchronous method. This method is used to query to the container to read items from it.
    8. The updateProduct() is an asynchronous method that is used to update the product based on the search criteria as id. This method also accepts ProductDetails object to update the searched product based on id.  

Step 6: In the controllers folder, add a new file and name it as appcontroller.ts. Add the following code in it:

// 1. import Request and Response objects from express
import { Request, Response } from "express";
// 2. import the ProductDetails class
import { ProductDetails } from './../models/productsdetails';
// 3. import the Dao class
import { Dao } from "./../models/dao";
// 4. The AppController class
export class AppController {
    // 4.1 Define an instance of the Dao class
    private dao: Dao;
    constructor() {
        this.dao = new Dao();
    }
    // 4.2 the init method accepts initDbAndContainer() 
      method to create database and container
    async init() { 
        await this.dao.initDbAndContainer();
    }
   // 4.3 this method accepts queryData() method to return query result  
    async getData(request: Request, response: Response) {
        const products = await this.dao.queryData();

        response.send({ data: products });
    }
    // 4.4 this method accepts the ProductDetails in the request body and 
create product by accepting addProduct() method
    async addProduct(request: Request, response: Response) {
        const body = request.body;
        console.log(`Received Body ${JSON.stringify(body)}`);
        const product = new ProductDetails(
            body.ProductId,
            body.ProductName,
            body.CategoryName,
            body.SubCategory,
            body.Description,
            body.Manyfacturer,
            body.Price
        );
        console.log(`Product ${JSON.stringify(product)}`);
        let data = await this.dao.addProduct(product);
        response.send({ data: data });
    }
    // 4.5 this method is used to update the product
    async updateProduct(request: Request, response: Response) {
        const body = request.body;
        const id = request.params.id;
        console.log(`Received Body ${JSON.stringify(body)}`);
        const product = new ProductDetails(
            body.ProductId,
            body.ProductName,
            body.CategoryName,
            body.SubCategory,
            body.Description,
            body.Manyfacturer,
            body.Price
        );
        console.log(`Product ${JSON.stringify(product)}`);
        let data = await this.dao.updateProduct(id,product);
        response.send({ data: data });
    }

}

Listings 5: The AppController class that accepts Dao class

The above code has the following specifications (Note: The following line numbers matched with comments number applied on above code)

  1. imports the Request and Response objects from express package. These objects represents express REST API requests and responses.
  2. imports ProductDetails class
  3. The Dao class is imported so that its methods can be accessed.
  4. The AppController class. This class contains all asynchronous methods and does the following:
    1. The constructor creates an instance of the Dao class
    2. init() is an asynchronous method. This method access initDbAndContainer() method of the Dao class to create database and container.
    3. getData() is an asynchronous method. This method accepts  Request and Response objects. This method accesses queryData() method from the Dao class and responds with the resultant products.
    4. addProduct() is an asynchronous method. This method accepts the ProductDetails in request body. The data from request body is read and stored in the local ProductDetails object. This object is then passed to the addProduct() method of the Dao class to create new product in the container. This method responds the created product in the container.
    5. updateProduct() is an asynchronous method. This method accepts the ProductDetails in request body. The data from request body is read and stored in the local ProductDetails object. The request contains the id parameter. This parameter is used to search the product to be updated.  This method responds the updated product in the container.
Step 7: In the root of the project, add a new file and name it as server.ts. In this file, add the following code:

// 1. imports express, bodyparser and cors packages
import * as  express from 'express';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
// 2 imports the AppController
import { AppController } from "./controllers/appcontroller";

// 3. an instance of the express and adding bodyParser and cors middlewares
const instance = express();
instance.use(bodyParser.json());
instance.use(bodyParser.urlencoded({ extended: false }));
instance.use(cors());

// 4. instance of the AppController
const appController = new AppController();
// 5. accessing the init() method
appController.init().then(() => { 
console.log('database and container created Successfully'); }).catch(() => {
    console.log('database and container creation failed');
    process.exit(1);
});

// 6. API methods for get, post and put
instance.get('/api/products', 
(req,res,next)=>appController.getData(req,res).catch(next));

instance.post('/api/products', 
(req,res,next)=>appController.addProduct(req,res).catch(next));

instance.put('/api/products/:id', 
(req,res,next)=>appController.updateProduct(req,res).catch(next));

// 7. listen on the port
instance.listen(9078, () => {
    console.log('started on port 9078');
});

Listing 6: The server code with REST API

The above code have following specifications (Note that the following line numbers matched with comments applied on code):
  1. Imports required packages like express, body-parser and cors.
  2. Imports AppController class
  3. Defines instance of express and set required middlewares.
  4.  Instance of the AppController class
  5. init() method of the AppController class is accessed. This creates database and the container.
  6. REST API methods are used to access various methods from the AppController class.
  7. Starts listening on the port
Step 8: Run the application by running the following command on the command prompt:

npm run dev

This will show the following result: 


Image 3: The result showing database and container created 

The database and the container is created.  Visit back to the portal, it will show the Database and container created as shown in the following image


Image 4: The portal shows the Database and the container 

Step 8: Open Postman or Fiddler client (or any other REST client) to test the REST API. I am using Postman. Open Postman and enter the following URL in the address bar

http://localhost:9078/api/products

and make a Get request, it will show the result as empty result. Now make a post request with data as shown in the following image:


Image 9: The Post request

The following image will show the record added in CosmosDB :



Image 10: The data in the CosmosDB

Conclusion:

In this tutorial, we saw the steps for accessing SQL API of CosmosDB using Node.js 






About The Author

Mahesh Sabnis is a Microsoft MVP having over 18 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). He also blogs regularly at DotNetCurry.com. Follow him on twitter @maheshdotnet

No comments: