Categories
Codes Javascript MongoDB Node.js

How to Create REST API Using Node.js – Part 2

“Experts often possess more data than judgment.”

― Colin Powell

Introduction

In the previous post, we learned about creating a new Node.js API project and setting up the necessary libraries. We created the endpoints, tested it on Postman, and got the dummy results successfully.

Let’s make our API more useful. In this second post, we will learn how to create, read, update, and delete data from MongoDB. And then, we will integrate the API from the previous post with it. And like the previous post, I will also share the complete project on GitHub at the end of the post.

Requirements

MongoDB

“MongoDB is a cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with optional schemas. MongoDB is developed by MongoDB Inc. and licensed under the Server Side Public License.”

Wikipedia

You can find the installation steps here.

MongoDB GUI

If you would like to browse your database with a GUI, you can download NoSQL Booster or Robo3T.

Part 2 – Read, Insert, Update, Delete Data Using MongoDB

Prerequisites
Start Fresh (if you want to)

If you’d like to start fresh to learn this topic, please clone from this repository in GitHub.

Install MongoDB Library

In the project root, run this command: npm i mongodb. You should see something like this screenshot below if everything goes smoothly:

Make sure MongoDB service is running

Open your terminal, then execute mongo. If it shows like the screenshot below, then it means the MongoDB is running well.

You can also put MongoDB query to modify the database. But, personally I prefer using the NoSQL Booster to check them out though.


Connecting to MongoDB

We will create a script named mongoDbConnector.js. This script will contain everything related to MongoDB.

Create a mongodb connector module

First, we have to connect to the service to use its capabilities. See this snippet below on mongoDbConnector.js:

// mongoDbConnector.js
const mongodb = require('mongodb');

class MongoDbConnector {
  constructor(config){
    Object.assign(this, { config });
  }

  connect(){
    const { host, name } = this.config;
    const options = {
        useUnifiedTopology: true,
        useNewUrlParser: true
    };

    mongodb.MongoClient.connect(host, options, 
      (err, client) => {
        if (err) {
          console.log(err);
        }
        else {
          console.log("Connected successfully to DB");
          const db = client.db(name);
          Object.assign(this, { db, client });
        }
     });
  }

  disconnect(){
    this.client.close();
    console.log("Disconnected DB successfully!");
  }
}

module.exports = MongoDbConnector;

First we create a MongoDbConnector class, where all MongoDB operations will be executed. Then, we create to public functions, connect() and disconnect(). We will use connect() when starting the API service, and disconnect() when the service is killed.

In connect(), we use the MongoClient.connect() from the mongodb library. The function needs some arguments: host, options, and a callback function. In the callback function, if it connects successfully, we use client.db(name) to use the database we want. Finally, we assign db and client as properties of the MongoDbConnector object.

Then, in disconnect(), we want to close the connection to the database server. Executing this.client.close() will terminate the connection.

Lastly, export the class as a module using module.exports, because we will use it from index.js.

IMPORTING mongodb connector

OK, we have now set MongoDbConnector class. Now, let’s go back to index.js, where we will import the MongoDbConnector class and create an object from it. See this snippet:

// index.js
...
const port = 3000;
const MongoDbConnector = require('./mongoDbConnector');
const mongoDbConnector = new MongoDbConnector({
    name: 'cil-rest-api',
    host: 'mongodb://localhost:27017'
});
mongoDbConnector.connect();

app.use(bodyParser.json());

app.post('/user', (req, res) => { ... });
...
app.listen(port, () => {...});

['SIGINT', 'SIGTERM'].forEach((signal) => {
    process.on(signal, async () => {
        console.log("Stop signal received");
        mongoDbConnector.disconnect();
        console.log("Exiting now, bye!");
        process.exit(0);
    });
});

First, we import the class: require('./mongoDbConnector').

Then, we create a MongoDbConnector object by also passing the configuration object as the argument: const mongoDbConnector = new MongoDbConnector(config); The configuration properties are only the database name and host for now.

Lastly, we call mongoDbConnector.connect() to connect to the database server.

Where do we use .disconnect()? See the bottom part of the snippet. The function will be called if we decide to stop the API service through SIGINT or SIGTERM. Just before process.exit(0) is executed, we must close all connections to the remote server cleanly.

Test the code

If everything is done correctly, you should see the result pretty much like this picture below. Start the app with npm start in the terminal. To execute the stop signal, type Ctrl+C.

Connected to MongoDB successfully!

Create New User
mongodb connector

Let’s get back to mongoDbConnector.js. We will add the code to save the new users in the database. See this snippet below:

// mongoDbConnector.js

...
class MongoDbConnector {
  ...
  disconnect() { ... }

  async insertOne(collection, data){
    try {
      const res = await this.db.collection(collection)
        .insertOne(data);
      const textRes = res.result.ok ?
        `Success insert data to collection ${collection}!`: 
        `Failed insert data to collection ${collection}!`;
      console.log(textRes);
      return textRes;
    }
    catch(err) {
      throw err;
    }
  }
}

The insertOne function is an asynchronous function, because the results will be returned longer since it is on a different service. This function requires the parameters collection and data. Notice that there is no need to specify creating a new collection in MongoDB, because MongoDB automatically creates a new collection if it does not exist.

Because this operation depends on the status of MongoDB service, it is a good practice to wrap the function inside a try-catch. Inside the try, we call the function to insert data:
this.db.collection(collection).insertOne(data);
If the data insertion is successful, it returns a success text. If it fails, it returns a failure text.

Then, inside the catch, we throw the error if something bad happened to the MongoDB when executing the insertOne() code.

Index.js

Next, we must modify POST /user, to enable the API to insert data to the database. See this snippet below:

// index.js

...

const collection = 'cil-users';

...

app.post('/user', async (req, res) => {
  const result = await mongoDbConnector
    .insertOne(collection, req.body);
  res.send(result);
});

...

We only need to specify which collection that will be inserted, let’s name it “cil-users”. And we put it before the initializations.

Next, we modify app.post('/user'). Add async to the callback function, and then call await mongoDbConnector.insert(). The collection is “cil-users” and the data is obtained from req.body. After getting the result, we return it as the response from this endpoint by using res.send().

Test the code

Run npm start on the terminal and do some tests. If you do the steps correctly, you should have the results from these screenshots:

POST /user works, and it returns successfully!

Start your NoSQL Booster or Robo 3T. You should see that the new database “cil-rest-api” is created with the collection “cil-users”.

DB “cil-rest-api” is automatically created with collection “cil-users”

Lastly, check if the data is inserted successfully by running this command in NoSQL Booster:

See the inserted data by running the NoSQL command above.

Get User (Many)
MongoDb Connector

To get the data of multiple users, we will create the function find(), and we will add it to mongoDbConnector.js. See this snippet below:

// mongoDbConnector.js

...

class MongoDbConnector {
  ...

  async find(collection, filter) {
    try {
      const res = await this.db.collection(collection)
.find(filter);
      return res.toArray();
    }
    catch (err) {
      throw err;
    }
  }

  ...
}

First, we create a find() function. It needs two parameters: collection and filter. filter is the query for finding the data in JSON format, for example{ username: 'johndoe' }.

Next, we create a try-catch block to detect any errors that might happen. Inside the block, we call the function db.collection().find() with filter argument. This function will return a list of data which fulfil the filter. Lastly, the result will be returned after converted to an array of object with toArray().

Index.js

Now, let’s integrate the find() function from MongoDbConnector to the endpoint GET /user. Here is how we do it:

// index.js

...

app.get('/user', async (req, res) => {
  const result = await mongoDbConnector.find(collection, {});
  res.send(result);
});

...

So, we implement it in app.get("/user"). First, we change the callback function to be an asynchronous function by adding async. Then, we call mongoDbConnector.find(collection, {}); we just wrote before to get the list of users. Finally, we return result as the response for this endpoint.

Test the code

Now, open your Postman and call
GET http://localhost:3000/user. If you do it correctly, you should see something like this:

GET /user

Get User (One)
MONGODB CONNECTOR

Let’s add a function called findOne(). See this snippet below:

// mongoDbConnector.js

class MongoDbConnector {
  ...
  async findOne(collection, filter) {
    try {
      const res = await this.db.
collection(collection).findOne(filter);
      return res || 'Data not found.';
    }
    catch (err) {
      throw err;
    }
  }
  ...
}

findOne() is also an asynchronous function, because we would need to wait for the response from the database. It needs two arguments: collection and filter.

In the function, just like insert() before, we use try-catch . Then we call the collection to call findOne() with the filter sent from the parameters. If the document is found successfully, return the document, or it will return “Data not found.”

index.js

Back to index.js. Now, we will enable the findOne() code in MongoDbConnector to be accessed from GET /user/:id. See this snippet:

// index.js
...
const MongoDbConnector = require('./mongoDbConnector');
const { ObjectId } = require('mongodb');
...
app.get('/user/:id', async (req, res) => {
  const filter = { _id: ObjectId(req.params.id) };
  const result = await mongoDbConnector
    .findOne(collection, filter);
  res.send(result);
});
...

We get this task done by modifying app.get("/user/:id"). First, set the callback function as an asynchronous function. Then, we call mongoDbConnector.findOne() with the arguments collection and filter.

Because we would like to find the user by id, we set filter as
{ _id: ObjectId(req.params.id) }. Note that the id here is auto-generated from MongoDB, so it has to be in the format of ObjectId. Which is why we need to import ObjectId from mongodb library.

Lastly, we get the result from the connector and pass it back as a response through res.send().

Test the code

Switch to Postman, and hit GET http://localhost:3000/user/<_id>. You can get the _id from the database. If it finds the document, it will show something like this:

GET /user/:id succeeds

If the document is not found, it will show that “No data is found”, like this screenshot below:

GET /user/:id fails (expected response)

Update Existing User
Mongodb connector

Now, we want to update existing users. Let’s create function updateOne() in mongoDbConnector.js. See this snippet below:

// mongoDbConnector.js

class MongoDbConnector {
  ...
  async updateOne(collection, filter, data) {
    try {
      const res = await this.db.collection(collection)
.updateOne(filter, { $set: data });
      const { nModified, ok } = res.result;
      return nModified && ok ?
        'Successfully updated data' : 
        'Failed to update data';
    }
    catch (err) {
      throw err;
    }
  }
  ...
}

Function updateOne() requires three parameters: collection, filter, and data. filter is for selecting which existing data we want to update, and data will add or replace the properties of the document selected by filter.

As usual, we add a try-catch block to catch errors if it happens. Inside the try block, we use .updateOne(filter, { $set: data }); to execute the update on the database. The second argument requires the expression $set to update the data. See the documentation for more details.

The result obtained from .updateOne() will be deconstructed to check whether the execution succeeded or not. If the value of nModified is more than 0 and ok is also more than 0, it will return “Successfully updated data”. If it isn’t, it will return “Failed to update data”.

INDEX.JS

Next, we move back to index.js to integrate updateOne() to PATCH /user/:id. See this snippet below:

// index.js

...

app.patch('/user/:id', async (req, res) => {
   const result = await mongoDbConnector.updateOne(
       collection,
       { _id: ObjectId(req.params.id) },
       req.body
   );
   res.send(result);
});

...

We add more details on app.patch('/user/:id'). Set the callback function as an asynchronous function by adding async. Then call mongoDbConnector.updateOne(). It requires the collection name, the filter by id, and the data to be added or replaced if the document is found.

Just as the same with the previous section Get User (One), finding by _id requires changing the format to MongoDB ObjectId(). The last data argument is taken from req.body, which is an object.

Finally, whatever result returned from mongoDbConnector.updateOne() will be sent as the response for PATCH /user/:id.

Test the code

Now, run npm start and open your Postman app and test the endpoint. First, get an existing document with GET /user/:id we wrote on the previous section. See this screenshot:

Test by getting a document first.

Second, do the update with PATCH /user/:id, you can put any update you want, it will add non-existent property and change the value of existing property:

Update data success!

Lastly, check the document with the same id. You will see the updated and added properties on the document:

Successfully getting updated data.

Delete Existing User
Mongodb Connector

Last part. We will add the delete part by adding deleteOne() on mongoDbConnector.js. See this snippet below:

// mongoDbConnector.js

class MongoDbConnector {
  ...

  async deleteOne(collection, filter) {
    try {
      const res = await this.db.collection(collection)
.deleteOne(filter);
      const { n, ok } = res.result;
      return n > 0 && ok ?
        'Successfully deleted data' : 
        'Failed to delete data';
    }
    catch (err) {
      throw err;
    }
  }

  ...
}

deleteOne() requires two parameters: collection and filter. By now, you should already know what these parameters do.

Inside the function add a try-catch block. Then, to delete the document in the database, use db.collection(collection).deleteOne(filter).

The result from this operation will be used to return “Successfully deleted data” if res.result.n > 0 && res.ok , and “Failed to delete data” if the condition is not fulfiled.

Index.js

Back to index.js, the request we use is DELETE /user/:id. So, we will modify app.delete() to become something like this snippet below:

// index.js

...

app.delete('/user/:id', async (req, res) => {
  const result = await mongoDbConnector.deleteOne(
    collection,
    { _id: ObjectId(req.params.id) }
  );
  res.send(result);
});

...

Set the callback function as an asynchronous function by adding async. Then, add mongoDbConnector.deleteOne() to call the function from mongoDbConnector.js we wrote before. The filter is _id, which is obtained from req.params.id. Lastly, we return result as the response of DELETE /user/:id.

Test the Code

These screenshots will give you a picture of what a successful DELETE /user/:id request should be:

GET /user/:id to make sure the document exists first.

First, we get any document inserted to the database by using GET /user/:id. Make sure the document exists, because we want to make sure it’s deleted later on.

DELETE /user/:id to delete document.

Next, we execute DELETE /user/:id. “Successfully deleted data” will show if the deletion executed successfully.

GET /user/:id again to confirm the document’s deletion.

Then, we check again if the deletion really worked. Execute GET /user/:id again. If “Data not found” shows up, then the operation is successful.

Conclusion

Phew, quite a long post, this one. I’m very sure we all learnt a lot this time also. Here’s a quick recap of the lessons learned:

  • MongoDB installation
  • Create a MongoDB Connector class
  • Connecting to MongoDB
  • Integrate some MongoDB CRUD operations in Node.js
  • Integrate MongoDB Connector to API routes

And here is the source code for this post. What have you missed? Or maybe do better? Let me know in the comments!

Next post in the series:
Part 3 – Caching With Redis

“Errors using inadequate data are much less than those using no data at all.”

― Charles Babbage

By Ericko Yap

Just a guy who is obsessed to improve himself. Working as a programmer in a digital banking company. Currently programming himself in calisthenics, reading books, and maintaining a blog.

Leave a Reply

Your email address will not be published. Required fields are marked *