FOR DEVELOPERS

Building Secure REST API in Node.js

Building Secure REST API in Node.js

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

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;

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

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}); })

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" }

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!"); } };

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);

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);

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;

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@&quot;]+(.[^<>()[]\.,;:\s@&quot;]+)*)|(&quot;.+&quot;))@(([[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)
}

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) =&gt; {
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) =&gt; {
    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) =&gt; {
    try {
    await Post.findByIdAndDelete(req.params.id);
    res.status(200).json(&quot;post has been deleted&quot;);    
    } catch (error) {
    console.log(error);
    }
}

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

}

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

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;

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;

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}); })

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.

Author

  • Building Secure REST API in Node.js

    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.

Frequently Asked Questions

Input validation ensures that the API only processes expected and properly formatted data, lowering the risk of security vulnerabilities such as SQL injection and XSS.

Keep track of software updates and apply them as soon as possible to ensure you have the most recent security patches. To automate the process of keeping dependencies updated, use package managers and dependency management tools.

Encryption protects sensitive data, such as passwords and financial information, by converting it into a coded format that unauthorized parties cannot easily understand.

There are several popular Node.js frameworks available for building RESTful APIs, and the choice of framework often depends on your specific requirements and preferences. However, some of the most commonly used and well-regarded Node.js frameworks for building RESTful APIs are:

  • Express
  • Koa
  • Hapi
  • Nest.js

It's always a good idea to research and evaluate multiple frameworks before selecting the one that best fits your needs.

Hashing is used to securely store sensitive data such as passwords. It converts plain text into a one-of-a-kind string of characters that cannot be reversed.

REST (Representational State Transfer) is a software architectural style for building distributed systems, including web services like RESTful APIs. While REST itself does not have built-in security features, it provides a set of constraints and principles that can be used to design secure web services.

When building a RESTful API, it is up to the developer to implement security measures to protect against common security threats, such as unauthorized access, injection attacks, and cross-site scripting (XSS) attacks. Some common security measures that can be implemented in a RESTful API include:

  • Authentication
  • Encryption
  • Authorization
  • Input validation

In summary, while RESTful APIs do not have built-in security features, developers can implement a variety of security measures to ensure the safety and integrity of their web services. It's important to carefully consider the security requirements of your API and choose appropriate security measures to mitigate potential security risks.

View more FAQs
Press

Press

What’s up with Turing? Get the latest news about us here.
Blog

Blog

Know more about remote work. Checkout our blog here.
Contact

Contact

Have any questions? We’d love to hear from you.

Hire remote developers

Tell us the skills you need and we'll find the best developer for you in days, not weeks.