How to build a REST API using NodeJS

Mukesh - Jan 22 '22 - - Dev Community

πŸ‘‹ Hey everyone, I know it's been a long since I posted a new blog πŸ˜…. πŸ‘€ So in this blog post we are doing to build a REST API that would serve as a source of motivation for developers using NodeJS and MongoDB. So let's get started πŸ„β€β™‚οΈ

What's an API? πŸ€”

API stands for "Application Programming Interface" which is a tool that allows two applications to talk to each other πŸ“ž. Let's understand the meaning of API by some real-life examples ✨

So you have built an amazing e-store application and you wanted other developers to build applications on it. Now you have to build some sort of software that communicates between your web service and the developer's application and that's where API comes in.

What's a REST API? πŸ€”

Now as you have let's talk something about "REST APIs". REST stands for Representational State Transfer, it's one of the most popularly known type of API architecture. These types of APIs follow the client-server model, where one program sends a request and the other response with some data.
The requests are HTTP methods such as POST, GET, PUT, DELETE...

You would have a more clear understanding of APIs and REST APIs when we build a project πŸ‘€. So what are we waiting for, let's dive started into coding πŸ‘¨β€πŸ’».

Setting up the project πŸ› 

Let's set up our project so that we can start coding πŸ‘¨β€πŸ’».

  1. Creating a separate folder for our project
   $ mkdir dev-credits-api
Enter fullscreen mode Exit fullscreen mode
  1. Navigate into the folder
   $ cd dev-credits-api
Enter fullscreen mode Exit fullscreen mode
  1. Initializing the project
   $ npm init
Enter fullscreen mode Exit fullscreen mode
  1. Installing the required packages
   $ npm install mongoose express dotenv cors

   # or

   $ yarn add mongoose express dotenv cors
Enter fullscreen mode Exit fullscreen mode
  • Express is the framework by which we are going to our REST API
  • Mongoose is the tool that we are going to use to communicate with our MongoDB database

    4.1. Installing nodemon as a dev dependency

     $ npm install nodemon -D
    
     # or
    
     $ yarn add nodemon -D
    
    • Nodemon is used for automatically restarting the server on file changes detected in the directory. This would be helpful as we would not be restarting the server each time we do changes

Building the REST API πŸ‘¨β€πŸ’»

As we have completed the setup for our project, let's get started building the REST API.

Create a new file named index.js

Here is the boilerplate code for a basic express app

index.js

const express = require('express');

const app = express();

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

app.listen(port, async () => {
  console.log(`Server is running at port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Let's breakdown it into and understand each part:

  • We are requiring the express package into our file so that we can use it
  • We are assigning some value to the variable port, the port where our server would be running. You might be thinking why is there a process.env.PORT? πŸ€”. It's because during deployment on services such as Heroku the port number might vary, it may not be 3000 so we are telling that if there is a PORT environment variable then use that else use 3000
  • The last piece of code is telling to which port the server should listen, in our case it's the PORT variable

Let's add a new script named start to the package.json file which uses nodemon to automatically restart the server on file changes detected. So after the changes our scripts in package.json would look something like this:

"scripts": {
   "start": "nodemon index.js"
}
Enter fullscreen mode Exit fullscreen mode

Let's start our server by running the npm start command. The server would be running at http://localhost:3000. You prompted with an error something like this:

This is happening because we haven't defined the / (aka the root route)

HTTP methods explained

Let's take a break from coding and understand what do they do and what's the success and error status so that it would be easy for debugging 😎

GET

What it does: Request data from a specified resource

Successful response: 200 OK

Error response: 404 not found

POST

What it does: Send data to the server to create a new resource

Successful response: 201 Created

Error response: 404 not found or 409 conflict - if the resource already exists

PUT

What it does: Send data to the server to update a pre-existing resource

Successful response: 200 OK

Error response: 204 no content, 404 not found or 405 method not allowed

DELETE

What it does: Deletes a resource from the server

Successful response: 200 OK

Error response: 404 not found or 405 method not allowed

Check out http.cat for understanding what each HTTP status code means via funny cat images 😹

Adding routes πŸ›£

Routes are different URL paths of an express app that are associated with different HTTP methods, such as GET, POST, DELETE, PUT.

Let's get started by creating / which sends "Hello, World!"

Add the below piece of code above the line where we declared the port variable

index.js

app.get('/', function (req, res) {
  res.send('Hello, World!');
});
Enter fullscreen mode Exit fullscreen mode

Let's breakdown this piece of code:

  • The get method specifies the HTTP method for that route. You could use other HTTP methods like post, delete...
    • There is a special routing method all which is used for the routes which handle all kinds of HTTP methods
  • There is a callback method that is called when the server receives a request from that endpoint with that specified HTTP method

πŸ₯³ Horray! "Hello, World" is now visible in the / route

Setting up MongoDB

Let's get in the MongoDB database now 😎.

Head over MongoDB and sign up/sign in and create a new project

You could your co-worker into the project if you wanted too.

After the creation of the project, click on Build a Database

You would be shown with a screen something like this:

Let's go ahead and choose the free plan πŸ‘€

You would be shown some more options about the cloud provider and the location

Let's choose the nearest region and move forward.

You would be asked to create a user. This is required as you would need the username and password to generate a connection URL which is then used to connect MongoDB with your NodeJS app.

The creation of the cluster would take 1 - 3 minutes. So let's grab a cup of coffee until then β˜•. Ahh... it's been successfully created so let's get back to coding πŸ‘¨β€πŸ’»

Click on Connect

Click on Connect your application

Copy the connection URL

Create a .env file and replace <password> with the password of the user which you have replaced previously

MONGODB_URL="mongodb+srv://kira272921:<password>@dev-credits-api.t5tkf.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"
Enter fullscreen mode Exit fullscreen mode

Let's head back to the good old index.js file

Connecting Express app to MongoDB

Let's start by requiring mongoose and dotenv

const mongoose = require('mongoose');
const dotenv = require('dotenv');
Enter fullscreen mode Exit fullscreen mode

Let's configure dotenv as well

dotenv.config();
Enter fullscreen mode Exit fullscreen mode

Let's finally add the piece of code which connects our express application to MongoDB

mongoose
  .connect(process.env.MONGODB_URL, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((err) => {
    console.log(err);
  });
Enter fullscreen mode Exit fullscreen mode

The index.js file show looks something like this now

index.js

const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');

dotenv.config();

const app = express();

mongoose
  .connect(process.env.MONGODB_URL, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((err) => {
    console.log(err);
  });

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

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

app.listen(port, async () => {
  console.log(`Server is running at port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

πŸ₯³ We successfully connected our express app to the MongoDB database.

Creating Schema and Model πŸ“

A Schema is the structure of the documents in our database. It tells what fields are required, what's the data type of each field.

A model provides a programming interface for interacting with the database (read, insert, update, etc).

Let's create a new folder named model and inside it let's create a model.js where we will define our schema

model/model.js

const mongoose = require('mongoose');

const devCredits = new mongoose.Schema({
  credits: {
    type: Number,
    required: true,
  },
  id: {
    type: Number,
    required: true,
  },
});

module.exports = mongoose.model('devCredits', devCredits);
Enter fullscreen mode Exit fullscreen mode

Let's breakdown it down and understand

  • We imported the mongoose package into the model/model.js file
  • We created a new schema named devCredits. The structure has the credits and id. Credits are the number of dev credits the person has and the id is the discord id of the user (This API was initially created for a discord bot Dev credits bot so the schema of the database is kinda based on discord πŸ€·β€β™‚οΈ)
  • We have finally created a model named "devCredits"

Adding more features 😎

Let's add more routes to our REST API. Let's add routes where we can get the total dev credits of a user via their discord ID and give dev credits to other users using another route.

Giving dev credits to other devs

Let's import our model which we have just created into the index.js file.

const devCredits = require('./model/model.js');
Enter fullscreen mode Exit fullscreen mode

Let's add a new POST route in the index.js file

app.post('/post', function (req, res) {
  const credit = new devCredits({
    id: req.body.id,
    credits: req.body.credits,
  });

  devCredits.countDocuments({ id: req.body.id }, function (err, count) {
    if (count > 0) {
      devCredits.findOneAndUpdate(
        { id: req.body.id },
        {
          $inc: {
            credits: req.body.credits,
          },
        },
        { new: true },
        (err, devCredit) => {
          if (err) {
            res.send(err);
          } else res.json(devCredit);
        }
      );
    } else {
      credit.save((err, credits) => {
        if (err) {
          res.send(err);
        }
        res.json(credits);
      });
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Let's understand what exactly is going on:

  • We have created a new POST route (/post)
  • We validate the data which we receive from the client using our model
  • In the next piece of code we are checking if the user (user id) already exists in the database or not
    • If exists then we are going to increment the credits value
    • Else we are going to create a new document with the user id and add the credits

How to test the API?

We have successfully created added a new feature in our API πŸ₯³. But wait how are we going to test it out πŸ€”

πŸ‘€ We are going to use a VSCode extension called Thunder Client, which is used for API testing. So let's quickly download it and test our new feature in our API πŸ₯³.

After the completion of the download, you are going to see a thunder icon in your sidebar πŸ‘€

Click the thunder icon and you are going to see a section something like this

Click on New Request. You would be prompted to screen something like this

Let's test out our /post route now πŸ₯³. Change the URL in the input box from https://www.thunderclient.com/welcome to HTTP:localhost:3000/post

Change the HTTP method from GET to POST

Navigate to the Body tab, this is the section where we are going to write the body of the request.

I have added my discord ID and gave 100 dev credits to it, cuz why not

Let's click and hope that it works 🀞

πŸ₯πŸ₯πŸ₯πŸ₯πŸ₯ and we got an error

This happened because we didn't have any middleware so let's them quickly

index.js

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
Enter fullscreen mode Exit fullscreen mode

NOTE: We had installed cors as a separated package, so don't forget to import it as well

Let's try again now so that it works now 🀞

πŸŽ‰ TADA! We have successfully created our first feature in the API which interacts with the MongoDB database

Getting the total dev credits of a user

Let's import our model which we have just created into the index.js file.

const devCredits = require('./model/model.js');
Enter fullscreen mode Exit fullscreen mode

Let's add a new route in the index.js file

app.get('/get/:id', function (req, res) {
  devCredits.find({ id: req.params.id }, { _id: 0, __v: 0 }, (err, data) => {
    if (err) {
      res.json(err);
    }
    res.json(data);
  });
});
Enter fullscreen mode Exit fullscreen mode

Let's breakdown this down

  • We have created a new route with the GET method
  • We are finding in the database for the ID given in the parameters

Let's test it out again using Thunder Client πŸ‘€.

πŸŽ‰TADA! It's works

Cleaning up the codebase

Let's clean up the codebase a bit πŸ˜….

Let's create a new folder called routes and inside it let's create a new file router.js which contains the routes

routes/router.js

const router = require('express').Router();
const devCredits = require('../model/model.js');

router.get('/get/:id', function (req, res) {
  devCredits.find({ id: req.params.id }, { _id: 0, __v: 0 }, (err, data) => {
    if (err) {
      res.json(err);
    }
    res.json(data);
  });
});

router.post('/post', function (req, res) {
  const credit = new devCredits({
    id: req.body.id,
    credits: req.body.credits,
  });

  devCredits.countDocuments({ id: req.body.id }, function (err, count) {
    if (count > 0) {
      devCredits.findOneAndUpdate(
        { id: req.body.id },
        {
          $inc: {
            credits: req.body.credits,
          },
        },
        { new: true },
        (err, devCredit) => {
          if (err) {
            res.send(err);
          } else res.json(devCredit);
        }
      );
    } else {
      credit.save((err, credits) => {
        if (err) {
          res.send(err);
        }
        res.json(credits);
      });
    }
  });
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

We have imported the routes/router.js file into the index.js file and used it

index.js

const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const cors = require('cors');

dotenv.config();

const router = require('./routes/router.js');

const app = express();

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

mongoose
  .connect(process.env.MONGODB_URL, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((err) => {
    console.log(err);
  });

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

app.use(router);

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

app.listen(port, async () => {
  console.log(`Server is running at port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Let's test it out so that we are sure that our code and we didn't mess up by cleaning up the mess πŸ˜†

πŸ₯³ Horray! There isn't any error and the code still works as it was before

πŸ˜… Doesn't routes/router.js seem kinda filled up with the logic and make it kinda messy?

Let's create a new folder named controllers. In this folder, we will store the logic related to each route.

Let's get started by creating a new file in the controllers folder named getCredits.js and postCredits.js which contains the logic related to the /get route and /post route respectively

controllers/getCredits.js

const devCredits = require('../model/model.js');

const getCredits = (req, res) => {
  devCredits.find({ id: req.params.id }, { _id: 0, __v: 0 }, (err, data) => {
    if (err) {
      res.json(err);
    }
    res.json(data);
  });
};

module.exports = getCredits;
Enter fullscreen mode Exit fullscreen mode

controllers/postCredits.js

const devCredits = require('../model/model.js');

const postCredits = (req, res) => {
  const credit = new devCredits({
    id: req.body.id,
    credits: req.body.credits,
  });

  devCredits.countDocuments({ id: req.body.id }, function (err, count) {
    if (count > 0) {
      devCredits.findOneAndUpdate(
        { id: req.body.id },
        {
          $inc: {
            credits: req.body.credits,
          },
        },
        { new: true },
        (err, devCredit) => {
          if (err) {
            res.send(err);
          } else res.json(devCredit);
        }
      );
    } else {
      credit.save((err, image) => {
        if (err) {
          res.send(err);
        }
        res.json(image);
      });
    }
  });
};

module.exports = postCredits;
Enter fullscreen mode Exit fullscreen mode

routes/router.js

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

const devCredits = require('../model/model.js');
const getCredits = require('../controllers/getCredits.js');
const postCredits = require('../controllers/postCredits.js');

router.get('/get/:id', getCredits);

router.post('/post', postCredits);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Phew, that was a lot of work 😹

Adding rate limit

You don't want some random guy to just spam your entire database πŸ˜†. So let's add a rate limit to our API when restricts the client to perform only a few requests every x minutes

Let's install express-rate-limit package

$ npm install express-rate-limit

# or

$ yarn add express-rate-limit
Enter fullscreen mode Exit fullscreen mode

Let's create a middleware folder that contains all the middlewares of our API. Create a file named rateLimiter.js under the middleware folder

middleware/rateLimiter.js

const rateLimit = require('express-rate-limit');

const rateLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 10,
  message: 'Bonk πŸ”¨',
});

module.exports = rateLimiter;
Enter fullscreen mode Exit fullscreen mode

Let's understand what this piece of code is doing?

  • We are importing the express-rate-limit package
  • The windowMs specifies the duration
  • The max specifies the max amount of requests the client can make in the duration specified
  • The message is the message which is shown to the client when he exceeds the max limit

So let's import into the index.js file and test it out

index.js

const rateLimiter = require('./middleware/rateLimiter.js');

app.use(rateLimiter);
Enter fullscreen mode Exit fullscreen mode

😹 I got bonked by myself

Deploying our API on Heroku

πŸ‘€ We have successfully built an API but how would other developers use it if it isn't deployed?

Let's deploy it on Heroku πŸš€.

Get started by initializing a git repository in the directory. Create a new GitHub repository and push your changes into that repository πŸ‘€

Let's create a new file named Procfile which is just a file that tells Heroku which command is need to be run. Add the below content to the Procfile file

web: node index.js
Enter fullscreen mode Exit fullscreen mode

NOTE: nodemon doesn't work in the production stage. It only works in the development stage, so we have to use the good old node index.js

Create an account on Heroku and click on Create new app, give some cool name to your API

Head over to the settings tab and click Reveal Config Vars

These are the environment variables

Add a new config var with the key as MONGODB_URL and the value as your MongoDB connection URL

Head back to the deploy tab and connect the GitHub repository which you have created just before to your Heroku application

Click the Deploy branch button. TADA πŸš€ You have successfully created a REST API and deployed it as well :D

The entire source code for this tutorial will be available on my GitHub https://github.com/Kira272921/dev-credits-api

Check out the API which we built today:

https://devcredits-api.herokuapp.com/

That's it for this blog folks 🀞. Meet y'all in the next blog post

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .