Leverage Turing Intelligence capabilities to integrate AI into your operations, enhance automation, and optimize cloud migration for scalable impact.
Advance foundation model research and improve LLM reasoning, coding, and multimodal capabilities with Turing AGI Advancement.
Access a global network of elite AI professionals through Turing Jobs—vetted experts ready to accelerate your AI initiatives.
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
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.
The following are the requirements for this tutorial;
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 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
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.:
javaScriptconst 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
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:
Our database is clearly connected, and our API is operational.
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.
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:
javascriptimport {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.
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@"]+(.[^<>()[]\.,;:\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) }
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.
Create a file called Post.js in the Controllers folder and paste the following code inside it.
javascriptconst {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;
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.
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.
javascriptconst 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.
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.
1. Register the endpoint
As you can see, the registering endpoint was successful, and a user has been created.
2. Login endpoint
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
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.
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.
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.
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.