Building Secure REST API in Node.js

7 min read

  • Languages, frameworks, tools, and trends

Many web and mobile applications now use REST APIs as their primary communication interface, allowing developers to access and exchange data over the internet. Building a secure REST API in Node.js necessitates a thorough understanding of how to protect sensitive data from malicious activities.

API security is critical in the development of web applications. APIs are the primary point of access to an application's data and services. As a result, they must be designed and constructed keeping security in mind.

Node.js is becoming a popular choice for API development. It includes a number of features that simplify and secure the development process.

In this article, we'll look at some best practices for creating secure REST APIs in Node.js

How to create a secure REST API in Node.js

1. Make use of HTTPS

HTTPS is the HTTP protocol's secure version. It enables encrypted communication between the client and the server, safeguarding data against interception and tampering.

HTTPS is required for any API that handles sensitive data or requires authentication. It ensures that data is transmitted securely and cannot be intercepted by any malicious activity.

2. Make use of Authentication

Authentication is a method of verifying a user's identity. It is a critical security measure for any API because it prevents unauthorized access to sensitive data.

You can use a variety of authentication methods in Node.js, including OAuth, JWT, and API keys. Each of these methods has advantages and disadvantages, so it is critical to select the best one for your application.

3. Restrict access

Another important security measure is to restrict API access. It ensures that the API and its data are only accessible to authorized users.

You can use access control lists (ACLs) in Node.js to specify who can access the API and what they can do. This enables you to limit access to only those users who have the required permissions.

4. Validation of input

Input validation ensures that the data sent to an API is correct and secure. It guards against malicious users sending malicious or incorrect data to the API.

To perform input validation in Node.js, you can use a library named express-validator. This library enables you to validate API data and reject requests that do not meet the specified criteria.

5. Implement security best practices

Finally, while developing a REST API in Node.js, it is critical to adhere to security best practices. This includes implementing secure coding practices such as input validation, HTTPS, and authentication and authorization.

To securely store passwords, we will use the bcrypt library, and to authenticate users, we will use the JSON web token library. We will also interact with a MongoDB database using the Mongoose library.

The prerequisites for the API will be discussed in the following section.

Prerequisites

The following are the requirements for this tutorial;

  • Sound knowledge of JavaScript.
  • Sound knowledge of Nodejs and Express.js framework.
  • The latest version of Node.js is installed.
  • Sound knowledge of APIs, and how they work.

Setup the project

Make a new project directory and install the dependencies.

$ mkdir secure-blog

$ cd secure-blog

$ npm init

$ npm install express bcrypt jsonwebtoken mongoose dotenv

Create the project structure

Create the following files and directories in the project root directory.

/ ├── config
  └── config.js
  ├── controllers 
  ├── auth.js
  └── posts.js 
  ├── models 
  └── user.js 
  ├── routes 
  ├── auth.js 
  └── posts.js 
  ├── app.js 
  └── package.json
Plaintext

Configure the database

Make a folder called config. Create a file called config.js inside the config folder to set up our database connection. Fill in the blanks with the code below.:

javaScript

const mongoose = require("mongoose");

const CONNECTDB = (url) => {
 mongoose.connect(url).then(()=> {
    console.log("database is connected");
 }).catch((err)=>{
    console.log(err);
 })
};

module.exports = CONNECTDB;
JavaScript

Create a.env file to store our database URL. Here is what our database URL will look like.

MONGO_DB_URL =
 mongodb+srv://mbathi:<password>@cluster0.hex8l.mongodb.net/<name of the 
database>?retryWrites=true&w=majority
Plaintext

Create a server

Inside the directory, create a file called app.js. Write the following code inside it:

javascript
const express = require('express');
const app = express();
const dotenv = require("dotenv");
dotenv.config();
const CONNECTDB = require(“../config/config”);
const PORT  = process.env.PORT || 5000 ;



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

//Database Connection 
Connect.CONNECTDB(process.env.MONGO_DB_URL);

//listen to the port 
app.listen(PORT, ()=> {
    console.log(`listening to port ${PORT}`);
})
JavaScript

As you can see from the code above, we've just created a server file where all activities from other folders will be executed.

So, in order to run our app.js file, we must include some commands in the package.json in order to make it easier to run the program.

Change this in the package.json

javascript

"scripts": {
    "start": "node app.js"
  }
JavaScript

You can now run the command 'npm start' in your terminal and get the following response:

Securing Node.js RESTful APIs.webp

Our database is clearly connected, and our API is operational.

Setup authentication

Make a folder called Auth in the root directory. Inside it, make a file called auth.js and add the following code:

javascript
const jwt = require("jsonwebtoken");
require("dotenv").config();

const verifyToken = (req,res,next) => {
  const authHeader = req.headers.token;
  if (authHeader) {
    const token = authHeader.split(" ")[1];
    jwt.verify(token, process.env.JWT, (err, user) => {
      if (err) res.status(403).json("Token is not valid!");
      req.user = user;
      next();
    });
  } else {
    return res.status(401).json("You are not authenticated!");
  }
   };
JavaScript

module.exports = {verifyToken};

Authentication is the process of verifying a user's or system's identity. It entails verifying a user's or system's credentials to ensure that they are who or what they claim to be.

As you can see in our file when a user logs in, a token is passed, and this token is verified if it is truly valid using this middleware.

Create models

1. User Models

Create a folder in the route directory and name it Models, inside the folder create a file and name it User.js. Add the following code inside it add the following code:

javascript

import {Schema, model} from  "mongoose";
import bcrypt from "bcryptjs";



const UserSchema = new Schema({
fullname: {
    type:String,
    required:true
 },
 email: {
    type:String,
    required:true
 },
 password: {
  type: String,
  required: true
 }
});

UserSchema.pre("save", async function (next) {
    if (!this.isModified('password')) {
        return next();
    }
    const salt = await bcrypt.genSalt(10);
    this.password = bcrypt.hashSync(this.password, salt);
    next();
});

UserSchema.methods.matchPassword = async function(password) {
    return await bcrypt.compare(password,this.password);
};

export const User = model("User",UserSchema);
JavaScript

We've added two prototypes to our code above. The first prototype is for encrypting our password so that when it is stored, no one can see it. The second one is the matchPassword one, which checks to see if the password you entered matches the one in the database.

2. Post Models

Create a file called Post.js inside the Models folder and add the following code to it:

javascript
import {Schema, model} from  "mongoose";

const PostSchema = new Schema({
title: {
    type:String,
    required:true
 },
userId: {
    type:String,
    required:true
 },
description: {
  type: String,
  required: true
 }
});



export const Post = model("Post", PostSchema);
JavaScript

We have completed the models for our API and are now moving on to the most important part of the API, the controllers.

User controllers

In the root directory, create a folder called controllers, and inside it, create a file called User.js and write the following code:

javaScript
import {User} from "../models/User";
import {emailValidator} from "../validators/emailValidator";;
import jwt from "jsonwebtoken";


class UserControllers {
    // register method
   static Register = async (req,res,next) => {
   const {fullname,email,password,phoneNumber} = req.body;
    try {
       const user = await User.findOne({email:email});
       if(user) return res.status(500).json(“This user already exist”);
       if(!emailValidator(email)) return res.status(500).json(“enter a valid email”)
        const saveuser = await User.create({
         fullname,
         email,
         password,
         phoneNumber
        });
        await saveuser.save(); 
      res.status(200).json("User created");
    } catch (error) {
        next(error)
    }
}
//login method
static Login = async (req,res,next) => {
try {
if(!req.body.email || !req.body.password) return next(ApiError.NotFound("please input values"))
const user = await User.findOne({email:req.body.email});
if(!user) return res.status(400).json(“This user doesn’t exist”);
const isMatch = await user.matchPassword(req.body.password);
if(!isMatch) return res.status(400).json(“wrong password”)
const token = jwt.sign({id:user._id,email:user.email},”collo”);
const {password, ...otherDetails} = user._doc;
res.status(200).json({user:{...otherDetails,token}});
} catch (error) {
   next(error) 
 };
}

}

export const  {Register,Login,} = UserControllers;
JavaScript

As you can see in our code above, there are two methods: Register and Login, with an email validator in the Register method. The email validator function is shown below to ensure that users enter a valid email address.

javascript
export const emailValidator = (email) => {
   const  re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
   return re.test(email)
}
JavaScript

In the second method, the Login, as you can see, we first check if the password matches the password saved in the database; if it does, you will be logged in; if it does not, you will receive an error.

When we log in, a token is passed to us, and it is verified when we pass the function we defined in the auth.js file.

Post controllers

Create a file called Post.js in the Controllers folder and paste the following code inside it.

javascript

const {Post} = require("../Models/Post");



class PostController  {
     static CreatePost = async(req,res,next) => {
        try {
          const {title,description} = req.body; 
          if(!title || !description) return res.status(400).json(" please input values"); 
          const post = await Post.create({
          title,
          UserId:req.user.id,
          description,
          });
         
         res.status(200).json(post);
        } catch (error) {
          console.log(error);
            
        } 
    };
     static GetPostByUserId = async (req,res,next) => {
    try {
    const post = await Post.find({UserId:req.user.id});
    res.status(200).json(post);
    } catch (error) {
    console.log(error);  
    }
    }
    static GetPostById = async (req,res,next) => {
        try {
        const post = await Post.findById(req.params.id);
        res.status(200).json(post);
        } catch (error) {
        console.log(error);  
        }
    }
    static DeletePostById = async (req,res,next) => {
        try {
        await Post.findByIdAndDelete(req.params.id);
        res.status(200).json("post has been deleted");    
        } catch (error) {
        console.log(error);
        }
    }

    static UpdatePost = async (req,res,next) => {
    try {
        await Post.findByIdAndUpdate(req.params.id,{$set : req.body}) 
        res.status(200).json("post updated")
        } catch (error) {
       console.log(error);
    }
    }
}

export const {CreatePost,GetPostByUserId,GetPostById,DeletePostById,UpdatePost} =  PostController;
JavaScript

In the code above, we simply performed a CRUD operation on the Post methods, i.e. creating a post, updating a post, deleting a post, and retrieving a post based on id and userId.

We will discuss routers in the following section.

User routers

Make a folder in the root directory and name it, This folder will contain the routers. Make a file called User.js.
Add the following logic inside the file.

javascript

const express =  require("express");
const {Register,Login} = require("../controllers/User");
const router = express.Router();

router.post('/register',Register);
router.post('/login',Login);

module.exports = router;
JavaScript

After we finish creating the User routes, we will move on to the Post routers.

Post routers

Make a file called Post.js inside the Routers folder. Insert the following code into that file:

javascript
const express =  require("express");
const {CreatePost,DeletePostById,UpdatePost,GetPostById,GetPostByUserId} = require("../controllers/Post");
const {verifyToken} = require("../Auth/auth");
const router = express.Router();

router.post('/',verifyToken,CreatePost);
router.delete('/:id',verifyToken,DeletePostById);
router.put('/:id',verifyToken,UpdatePost);
router.get('/:id',GetPostById);
router.get('/userId',GetPostByUserId);


module.exports = router;
JavaScript

As you can see, we passed a middleware where we need to authenticate that the person creating it is the authorized owner to write the post in that account, the same as the deleting router, where, to delete a post one has to be authorized if he/she is the owner of the post. The same is true for updated posts.

When we are finished with the routers, we must include them in the main file, the app.js.

javascript
const express = require('express');
const app = express();
const UserRoute = require("./routers/user");
const PostRoute = require("./routers/post");
const CONNECTDB = require("./config/config");
const PORT  =  5000 ;



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

//Database Connection 
CONNECTDB("mongodb+srv://mbathi:shanicecole@cluster0.hex8l.mongodb.net/secure-blog?retryWrites=true&w=majority");

//Routes
app.use("/api/user",UserRoute);
app.use("/api/post",PostRoute);



//listen to the port 
app.listen(PORT, ()=> {
    console.log(`listening to port ${PORT}`);
})
JavaScript

Postman can now be used to test our API. Below are a few screenshots to see if our application is up and running.

Testing APIs

1. Register the endpoint

secure REST API with node.js.webp

As you can see, the registering endpoint was successful, and a user has been created.

2. Login endpoint

Node.js REST API.webp

As you can see, our login endpoint was successful, and we were given a token in the response, which will be used to authenticate a user if he or she wishes to delete, post, or update a post.

3. Create post endpoint

REST API in Node.js.webp

As you can see, we're getting an error message that says "you are not authenticated”. This is because we did not pass a token in our create post endpoint to authenticate the user.

Building Secure REST API in Node.js (2).webp

As a result, we added the token as shown in the screenshot above, and the post was successfully created. This works similarly to the delete and update endpoints where you must pass the token to authenticate the user.

Conclusion

To summarize, creating a secure API with Node.js necessitates attention to detail and the implementation of various security measures to protect against common threats. These safeguards include encrypting and hashing sensitive data, validating and sanitizing input, implementing authentication and authorization, and keeping software up to date with the most recent security patches. Regular security testing and having a plan in place for responding to security incidents are also essential for keeping the API secure.

How to Use JavaScript for Backend Development

Author
Collins Mbathi

Collins Mbathi is a software engineer and a technical writer with over three years of experience in the field. He has written several articles on software engineering and has been featured in several industry-leading publications. He is passionate about creating software that is both efficient and user-friendly.

Share this post