Subscribe Now

* You will receive the latest news and updates on your favorite celebrities!

Trending News

Blog Post

The Perfect CRUD API  ( Node.js + MongoDB + Express)
Node.js, Tutorials

The Perfect CRUD API ( Node.js + MongoDB + Express) 

If you are a developer, you must have come across the word API for quite some time. API stands for Application Programming Interface, which exposes certain functionalities of the application to the outside world. Node.js is by far the top choice among developers to build application on servers along with other dependencies like Express and MongoDB. But why exactly do we need an API ?

For instance, you want to have an application on server for profile management of the employees of your company which handles creation, updation and deletion of profiles. There are indeed certain ways you could accomplish that, but the most naïve way I can imagine is to serve a static HTML page from the server with some input forms embedded in it. Well, this approach seems to be perfect if you are a beginner, but if you have worked on some projects across multiple of platforms, chances are you won’t like this approach very much. An API can be used with multiple type of applications without the need to re-invent the wheel. If you have an API which handles profile management, you can consume that same API in variety of devices including Android, iOS, desktop-applications or web apps.

A better approach for this problem would be to expose certain APIs ( basically routes) in order to handle server functions (CREATE, UPDATE, DELETE) and decouple the server infrastructure from outside world.

Advantages of APIs

  1. Scalability : Server applications having API driven systems are easier and more efficient to scale
  2. De-coupling : The profile management application is entirely de-coupled from other systems as well as the frontend which improves the management of API.
  3. Different Code-base : We can develop the every sub systems using different architecture patterns, frameworks or languages, and make them work together as one application with ease.
  4. Implementation Details: We can hide the details of the implementation ( like implementing validation, content moderation before writing to Server database).
  5. Security: APIs if done correctly, tends to be more secure than monoliths serving static HTML pages.

Lets build the app now!

Pre-requisites

  • Basic Javascript knowledge.
  • An IDE environment for Node.js (with npm).
  • MongoDB installed in you system. if not installed, download from here https://www.mongodb.com/try/download/community.
  • It is also recommended that you download MongoDB Compass, a desktop application for MongoDB.

Step-1 : Creating a Node.js application

If you are using Node.js for the first time, I suggest you to find articles explaining the concept and working of event driven architecture of Node.js. Do some basic hello world stuff, and after you are comfortable with the syntax, proceed with this article.

If you are using VS-Code, which I recommend you should, Open VS-Code and browse to the folder where you want to create the project.

Open up a terminal ( Ctrl + Shift + ` ), and install required dependencies using this command.

npm init

Now, you get a couple of questions regarding the config of the project. You don’t literally need to specify anything now, but you can change it later from the package.json file. So, go on pressing enter until the command finishes. Then,

npm i express mongoose

This code installs express and mongoose dependencies required for us to handle Mongodb and HTTP requests.

( Optional )

npm i --save-dev nodemon

This is a dependency called nodemon which restarts the server whenever a change in code is detected. This comes handy if you don’t plan to restart the server for every minor change you make in the code.

Step-2 : Setting up the entry-point

Express is a back-end web application framework, which is created to handle requests made to a web server. There are other wonderful things express can do but they are not relevant for this article.

After installing dependencies for express and mongoose, we need to create an entrypoint file for our web application. Lets name it index.js (by convention), but you can name it anything you want with a .js extension.

index.js

//import required dependencies
const express=require('express');
const profiledb=require('./db/profile-database');
const profileRouter=require('./routes/profile-route');

//Initialising express app
const app=express();

//if you like versioning your api
const apiVersion='v1';

//Assigning port 
const PORT= process.env.PORT || 3000;

//enable json in request 
app.use(express.json())

//Registering all queues
function registerRoutesAndStartListening()
{
    //Routes for profile functions
    app.use(`/api/${apiVersion}/profile`,profileRouter);

    //Start listening for requests on PORT
    app.listen(PORT,()=>
        console.log("CRUD Server is listening on localhost:"+PORT))
}

//Connecting to all essential databases before firing up the API
//If your api supports multiple database, make sure to connect to all database before 
//starting server. 
// eg: Promise.all([db1.ConnectToDb(),db2.ConnectToDb(),db3.ConnectToDb()])

Promise.all([profiledb.ConnectToDb()])
    .then(()=>{
        console.log("Connected to All Databases")
        registerRoutesAndStartListening();
    })
    .catch((err)=>console.log(err));

    

This is a simple entry point for our application. If you pay close attention, you might notice, unlike other applications we are trying to initialize all essential services before listening to requests. This is a failsafe mechanism structured to handle database connection errors.

We import certain custom dependencies which was created as helper methods to handle customary functions like connection to database, routing requests, validation of requests, etc.

Step-3 : Routing Requests using Express

Routing is a very crucial and essential part of an API. We expose different functionalities of API with different routes. Express extends the routing functionality to a whole new level thus enabling developers to write well structured and feature packed code.

For eg: A GET request at /read returns a list of users. an it can be denoted as route like this

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

//Read route
router.post('/read',(req,res)=>{
   //code to read profile data from DB
   res.status(200).send(users);
})

This was a fairly simple example of how routing works. Now, we will extend this to handle other requests as well.

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

//Create Route
router.post('/create',(req,res)=>{
   //code to save profile data from DB
   res.status(200).send("Done");
})

//Read route
router.post('/read',(req,res)=>{
   //code to read profile data from DB
   res.status(200).send(users);
})

//update Route
router.post('/update',(req,res)=>{
   //code to update profile data from DB
   res.status(200).send("Done");
})

//delete Route
router.post('/delete ',(req,res)=>{
   //code to delete profile data from DB
   res.status(200).send("Done");
})

Due to limitations of the scope of this article, I will not be explaining the entire concept of routing , requests and responses. So if you are having trouble understanding the concept, I suggest you to first become comfortable with the topics then come back to this article for a better understanding.

I will be using a custom middle-ware I wrote for validation of requests.

profile-validator.js

const createValidator=({body},res,next)=>{
    if(!body)
        return res.status(400).send("No post Data")
    if(!body.name)
        return res.status(400).send("No Name specified for User")
    if(!body.email)
        return res.status(400).send("No Email specified for User")
    if(!body.designation)
        return res.status(400).send("No Designation specified for User")
    if(!body.createdAt)
       body.createdAt= Date()
    if(!body.isVerified)
        return res.status(400).send("No isVerified specified for User")

    next();
}
const readValidator=({params},res,next)=>{
    if(!params)
        return res.status(400).send("No Parameter Data")
    if(!params.name)
        return res.status(400).send("No Name specified for User")
    next();
}
const updateValidator=({body},res,next)=>{
    if(!body)
        return res.status(400).send("No post Data")
    if(!body._id)
        return res.status(400).send("No id specified for User")
    if(!body.createdAt)
       body.createdAt= Date()
    
    next();
}
const deleteValidator=({body},res,next)=>{
    if(!body)
        return res.status(400).send("No post Data")
    if(!body._id)
        return res.status(400).send("No id specified for User")
    
    next();
}
module.exports={createValidator,readValidator,updateValidator,deleteValidator};

So summing up the concept and implementation, here’s what your profile-route.js should look like.

profile-route.js

//import required dependencies
const router= require('express').Router();

//importing custom Validator
const profileValidator=require('../utils/profile-validator');

//Create Route
router.post('/create',profileValidator.createValidator,(req,res)=>{
   //Code to create user
 res.status(200).send("Done");
})

//Read Route
router.get('/read/:name',(req,res)=>{
    //Code to read user
 res.status(200).send(user);
})

//Read all Route
router.get('/read',(req,res)=>{
    //Code to read all user
 res.status(200).send(users);
})

//Update Route
router.post('/update',profileValidator.updateValidator,(req,res)=>{
    //Code to update user
 res.status(200).send("Done");
})

//Delete Route
router.post('/delete',profileValidator.deleteValidator,(req,res)=>{
   //Code to delete user
 res.status(200).send("Done");
})

module.exports=router;

This code is incomplete, as we have not implemented the code to create, read , update or delete user. We will implement this later in the article.

Step-4 : Designing the User Model

Designing a perfect model is I believe a very if not the most important part of the backend design especially in case of NoSQL databases like MongoDB. For the sake of simplicity, here we have a simple model for user including just name, designation, creation date and verification status.

So, here is the model I designed

 {
        name:name,                   //String 
        email:email,                 //String
        designation:designation,     //String probably part of an enum
        createdAt:createdAt,         //long number for timestamp
        isVerified:isVerified        //boolean
 }

Again, you can use any model you want according to your requirements but make sure you have all the fields named conventionally and has a pre-defined type to avoid parsing error later on. I recommend using typescript for enforcing statically typed objects and thus it provides another layer of security against parse errors.

For easy handling and object creation, I like to throw my model files into its own file and create a simple builder function to create models. If you are not performing request validation at the API routes, you should at least do that here (although validation at API gateway is more recommended).

profile.js

//Method to create Build Profile
const buildProfile=({
    name,
    email,
    designation,
    createdAt,
    isVerified    
})=>{
    return {
        name:name,
        email:email,
        designation:designation,
        createdAt:createdAt,
        isVerified:isVerified
    }
}

module.exports=buildProfile;

Step-5 : Connection to MongoDB Database

In this tutorial we will be using MongoDB as our primary database. No reason for selection, I just love it though. MongoDB is a NoSQL Database and if you are not familiar with these technologies, just remember NoSQL is basically a schema less database architecture, in which your data is arranged in form of trees and nodes. Basically you don’t have to worry about the datatype or number of fields your model may have. You can have one user with 5 fields of data and other with 6 and the best thing is NoSQL allows us to do that with ease ( even though you shouldn’t) unlike conventional SQL databases where we have a predefined schema which allows nothing more or nothing less. Also, NoSQL tends to a little bit on the faster side if designed right (ONLY IF DESIGNED RIGHT).

So, here is a simple portable database connection I wrote built on top of mongoose (a MongoDB helper dependency).

NOTE: In order for MongoDB to work, either you should have installed MongoDB on your system or you can use MongoDB’s cloud database called Atlas. In the later case you’ll get connection string from Atlas dashboard for your cluster, but if you have installed MongoDB in your system, use the default connection string I have used below.

profile-database.js

const mongoose= require('mongoose');

//Change Database name if you want to 
const DB_NAME='crud_api_db';

const DEFAULT_CONN_STRING=`mongodb://localhost:27017/${DB_NAME}`

// A Promise to connect TO MongoDB
const ConnectToDb=(connectionString=DEFAULT_CONN_STRING)=>{
    return new Promise((resolve,reject)=>{
        mongoose.connect(connectionString,{useNewUrlParser:true,useUnifiedTopology:true},
            (err)=>{
                if(err)
                  return reject(err)
                return resolve();
            })
    })
}

module.exports={ConnectToDb};

usage :

const profiledb=require('./db/profile-database');

//Connecting to database like this
profiledb.ConnectToDb()
    .then(()=>{
        console.log("Connected to Database")
    })
    .catch((err)=>console.log(err));

//if You have another connection string than the default one, maybe from Atlas
profiledb.ConnectToDb('ANOTHER_CONN_STRING')
    .then(()=>{
        console.log("Connected to Database")
    })
    .catch((err)=>console.log(err));

We have implemented the database connection in our index.js if you remember. So, nothing more to do with database connection.

Step-5 : Data access methods

If you have studied system architecture or design, you must be familiar with the DAO (Data Access Objects). These are essentially objects or methods which facilitates transactions with database ( like fetching, inserting, updating, etc ). Even though we can implement these transactions in the route itself, its not recommended and will affect the performance and scalability of our application.

So, here I have created a separate file for every function I want, ( not the nicest approach, but helpful if I have to extend the application to implement some security measures like In-System validation or Logs, etc ).

data-access directory structure

profile-create.js

const ProfileModel=require('../../models/profile');
const profiledb=require('mongoose').connection.collection('profile_data');

const createUser=(reqData)=>{
    return new Promise((resolve,reject)=>{
    
    const profile=parseDatafromRequestData(reqData)
    profiledb.insertOne(profile)
        .then((value)=>resolve({result:value.result,profile:value.ops[0]}))
        .catch((reason)=>reject(reason))
    })
}

const parseDatafromRequestData=(reqData)=>{
    return ProfileModel({
        name:reqData.name,
        email:reqData.email,
        designation:reqData.designation,
        createdAt:reqData.createdAt,
        isVerified:reqData.isVerified
    })
}
module.exports=createUser;

profile-delete.js

const profiledb=require('mongoose').connection.collection('profile_data');
const {ObjectId}= require('mongodb')

const deleteUser=(reqData)=>{
    return new Promise((resolve,reject)=>{
    profiledb.findOneAndDelete({_id:ObjectId(reqData._id)})
        .then((value)=>resolve(value))
        .catch((reason)=>reject(reason))
    })
}

module.exports=deleteUser;

profile-read.js

const profiledb=require('mongoose').connection.collection('profile_data');

const readAllUsers=()=>{
    return new  Promise(async(resolve,reject)=>{
        const all_data = [];
        const results=await profiledb.find({});
        await results.forEach(doc=>all_data.push(doc))
        if(results)
           return resolve(all_data)
        return reject("No User Found")
    })
}
const readUser=(reqData)=>{
    return new  Promise(async(resolve,reject)=>{
        const all_data = [];     
        const results=await profiledb.find(reqData);
        await results.forEach(doc=>all_data.push(doc))
        if(results)
           return resolve(all_data)
        return reject("No User Found")
    })
}

module.exports={readAllUsers,readUser};

profile-update.js

const ProfileModel=require('../../models/profile');
const profiledb=require('mongoose').connection.collection('profile_data');
const ObjectId =require('mongodb').ObjectId

const updateUser=(reqData)=>{
    return new Promise((resolve,reject)=>{
    
    const profile=parseDatafromRequestData(reqData)
    profiledb.findOneAndUpdate({_id:ObjectId(reqData._id)},{$set:profile})
        .then((value)=>resolve(value))
        .catch((reason)=>reject(reason))
    })
}

const parseDatafromRequestData=(reqData)=>{
    return ProfileModel({
        name:reqData.name,
        email:reqData.email,
        designation:reqData.designation,
        createdAt:reqData.createdAt,
        isVerified:reqData.isVerified
    })
}
module.exports=updateUser;

Now that we have created the implementation of our data-access methods, Its time to rewrite our routes to call these functions.

profile-route.js

//import required dependencies
const router= require('express').Router();
const profileValidator=require('../utils/profile-validator');

//Importing the data-access methods
const CreateProfile=require('../data-access/profile/profile-create');
const UpdateProfile=require('../data-access/profile/profile-update');
const DeleteProfile=require('../data-access/profile/profile-delete');
const ReadProfile=require('../data-access/profile/profile-read');

//Create Route
router.post('/create',profileValidator.createValidator,(req,res)=>{
    CreateProfile(req.body)
    .then((value)=>res.status(200).send(value))
    .catch((reason)=>res.status(400).send(reason))
})
//Read Route
router.get('/read/:name',(req,res)=>{
     ReadProfile.readUser({name:req.params.name})
        .then(users=>res.status(200).send(users))
        .catch(error=>res.status(400).send(error))
})
//Read all Route
router.get('/read',(req,res)=>{
    ReadProfile.readAllUsers()
        .then(users=>res.status(200).send(users))
        .catch(error=>res.status(400).send(error))
})
//Update Route
router.post('/update',profileValidator.updateValidator,(req,res)=>{
    UpdateProfile(req.body)
    .then((value)=>res.status(200).send(value))
    .catch((reason)=>res.status(400).send(reason))
})
//Delete Route
router.post('/delete',(req,res)=>{
    DeleteProfile(req.body)
    .then((value)=>res.status(200).send(value))
    .catch((reason)=>res.status(400).send(reason))
})

module.exports=router;

And finally, we have finished the coding part!! And below is my directory structure

I prefer this directory structure, a little bit derived from clean- architecture and some parts just my preference.

Step-6 : The Result

What good is an application if it doesn’t work as expected. So, lets fire up the national API testing tool, The POSTMAN.

Before that, below are the routes

  1. Read all Users : GET http://<YOUR_IP>:3000/api/v1/profile/read
  2. Read Specific User : GET http://<YOUR_IP>:3000/api/v1/profile/read/<USER_NAME>
  3. Create User: POST http://<YOUR_IP>:3000/api/v1/profile/create
    • Post body:
      • name : String
      • email : String
      • designation : String
      • createdAt: long (timestamp)
      • isVerified: boolean
  4. Update User: POST http://<YOUR_IP>:3000/api/v1/profile/update
    • Post body:
      • _id : String
      • name : String
      • email : String
      • designation : String
      • createdAt: long (timestamp)
      • isVerified: boolean
  5. Delete User: POST http://<YOUR_IP>:3000/api/v1/profile/update
    • Post body:
      • _id : String

Lets hit these routes now and see what happens!

create user

read user:

update user:

delete user:

Everything seems to work fine. And please remember this is not yet production level and you may need to implement some business logic before exposing this API. There are indeed a few more improvements I can think of, but I’m gonna let you guys extend this project according to you requirements.

This project is available in my Github and yes I accept pull requests … 😊

Github Repo : https://github.com/JerrySJoseph/CRUD-API-Node

Thank You!

Related posts

Leave a Reply

Required fields are marked *