How To Build A CRUD Blog API With NodeJS, ExpressJS, and MongoDB

How To Build A CRUD Blog API With NodeJS, ExpressJS, and MongoDB

A beginner's guide to building RESTful CRUD APIs with ExpressJS and Mongoose

·

32 min read

Introduction

Welcome to our tutorial on how to build a CRUD (create, read, update, delete) blog API with NodeJS, ExpressJS, and MongoDB. In this tutorial, we will be building a RESTful API for a simple blog application that will allow users to create, read, update, and delete blog posts. We will be using NodeJS with the ExpressJS framework and a NoSQL database called MongoDB to store our data.

Before we dive into the tutorial, it's important to have a basic understanding of the following technologies:

  • NodeJS: NodeJS is a JavaScript runtime built on Chrome's V8 JavaScript engine. It allows developers to run JavaScript on the server side, creating the ability to build scalable, real-time web applications.

  • ExpressJS: ExpressJS is a popular web framework for NodeJS. It provides a set of functions and middleware to build web applications and APIs quickly and easily.

  • MongoDB: MongoDB is a NoSQL database that stores data in a flexible, JSON-like format called BSON. It is known for its scalability and ability to store large amounts of data efficiently.

In this tutorial, we will be covering the following topics:

  • Setting up a NodeJS and ExpressJS project

  • Connecting to a MongoDB database

  • Implementing CRUD functionality with ExpressJS routes

  • Testing the API with Postman

By the end of this tutorial, you will have a fully functional CRUD blog API that can be used for your own projects or as a starting point for building more complex APIs. Let's get started!

Setting Up The Development Environment

In this tutorial, we will be building a CRUD (Create, Read, Update, Delete) blog API using NodeJS, ExpressJS, and MongoDB. Before we start building the API, we need to set up our development environment.

Here are the steps to set up your development environment:

  • Install NodeJS and npm (Node Package Manager) on your system. You can download and install NodeJS from the official website https://nodejs.org/ or use a package manager like Homebrew (for macOS) or Chocolatey (for Windows).

  • Install MongoDB on your system. You can either download and install MongoDB from the official website https://www.mongodb.com/ or use a package manager like Homebrew (for macOS) or Chocolatey (for Windows).

  • Create a new folder for your project and navigate to it in your terminal.

  • Initialize your project by running npm init in the terminal and following the prompts. This will create a package.json file in your project folder.

  • Install the required dependencies by running npm install express mongoose. This will install the ExpressJS and Mongoose libraries, which we will be using to build our API.

  • Install a code editor. There are many code editors available, such as Visual Studio Code, Sublime Text, and Atom. Choose one that you are comfortable with.

  • Install Postman https://www.postman.com/ or any other API testing tool. We will use this tool to test our API endpoints.

That's it! You are now ready to start building your CRUD API with NodeJS, ExpressJS, and MongoDB.

Creating The Project Structure

Before we start building our CRUD API, it's important to first establish the project structure. This will help us keep our code organized and easy to maintain as we build out the different components of our API. Setting up the project

Setting up the project

To start, create a new directory for your project and navigate into it.

mkdir my-api
cd my-api

Then, initialize a new npm project by running npm init -y. This will create a package.json file in your project directory.

Next, we'll install the dependencies that we'll need for our project. Run the following command to install ExpressJS:

npm install express nodemon morgan

Creating the project structure

Now that we have our dependencies installed, let's create the project structure. We'll create the following directories and files:

  • config/: This directory will contain any configuration files for our project.

  • controllers/: This directory will contain the controllers for our API. The controllers will handle incoming requests and delegate to the appropriate service or model.

  • models/: This directory will contain the models for our API. The models will represent the data that we are storing in our database.

  • middlewares/: This directory will contain the middlewares for our API. The middlewares will have all the access for requesting an object, responding to an object, and moving to the next middleware function in the application request-response cycle.

  • routes/: This directory will contain the routes for our API. The routes will define the endpoints for our API and delegate to the appropriate controller.

  • utils/: This directory will contain the utils for our API. The utils will be used to add more functionality to our API.

  • validators/: This directory will contain the validators for our API. The validators will help ensure that the data being sent and received is in the correct format.

  • app.js: This file will contain the main code for our API. It will define the Express app, set up the routes, and start the server.

Your project structure should now look something like this:

my-api/
├── config/
├── controllers/
├── models/
├── middlewares/
├── routes/
├── utils/
├── validators/
└── app.js

Setting up the main file

Now that we have our project structure set up, let's start building out the main file for our API: app.js.

First, we'll require the necessary dependencies and create an instance of an Express app and also add some middlewares:

const express = require('express');
const morgan = require('morgan');

const app = express();

// Middlewares
app.use(express.json());
app.use(morgan('tiny'));

Then, we'll start the server by calling the app.listen() function:

app.listen(8000, () => {
  console.log('API server listening on port 3000');
});

Your app.js file should now look something like this:

const express = require('express');
const app = express();

app.listen(8000, () => {
  console.log('API server listening on port 3000');
});

In the package.json file add:

"scripts": {
  "start": "node index.js",
  "start:dev": "npx nodemon index.js",
},

Now we can locally run our API using

npm run start:dev

Setting Up The Database

Here we will setting up our database on MongoDB Atlas and connecting our API to the database using mongoose:

  1. Setting up the database on MongoDB Atlas:

    • First, go to https://www.mongodb.com/cloud/atlas and sign up for an account.

    • Click the "Build a New Cluster" button and follow the prompts to create a new cluster.

    • Once the cluster is set up, click the "Connect" button and choose "Connect Your Application".

    • Copy the connection string provided and save it for later use.

  2. Connecting the API to the database using mongoose:

    • In your API project, install the mongoose module using the following command:
```bash
npm install mongoose
```

* Next, create a `db.config.js` file in the `config` folder, then require **mongoose** in your API in the file and connect to the database using the connection string copied from MongoDB Atlas and add it to the variable named `MONGODB_URI`:


```javascript
const mongoose = require('mongoose');
require('dotenv').config();

MONGODB_URI } = process.env.MONGO_URI;

// Connect to mongodb
function connectToMongoDB() {
  mongoose.connect(MONGODB_URI);

  mongoose.connection.on('connected', () => {
    console.log('Connected to MongoDB successfully');
  });

  mongoose.connection.on('error', (err) => {
    console.log('Error connecting to MongoDB', err);
  });
}

module.exports = connectToMongoDB;
```

* Finally, require `connectToMongoDB` function into `app.js` and call it:


```javascript
const connectToMongoDB = require('./config/db.config');

// Connect to MongoDB database
connectToMongoDB();

// app.listen() code is here
```

This code uses the `mongoose.connect()` function to connect to the database specified in the `MONGO_URI` environment variable which will have a value like this `mongodb+srv://<username>:<password>@cluster0.yrrl4n4.mongodb.net/<database_name>?retryWrites=true&w=majority` . We are also using some options to improve the security and performance of the connection.

Creating The Models

In this section, we will focus on creating the models for our CRUD blog API using Node.js, Express.js, and Mongoose.

First, let's define the structure of a blog post. A blog post should have a title, content, and an author. We can represent this in our code using Mongoose schemas.

Create and blog.model.js file in the models directory and add this code:

const mongoose = require('mongoose');

const { Schema } = mongoose;
const { ObjectId } = Schema;

const BlogSchema = new Schema({
  id: ObjectId,
  title: { type: String, unique: true, required: true },
  description: { type: String },
  author: { type: String, required: true },
  authorId: { type: ObjectId, required: true },
  body: { type: String, required: true },
  state: { type: String, enum: ['draft', 'published'], default: 'draft' },
  readCount: { type: Number, default: 0 },
  readTime: { type: Number, default: 0 }, // Minutes
  tags: { type: Array },
  createdAt: { type: Date },
  updatedAt: { type: Date },
});

const Blog = mongoose.model('Blog', BlogSchema);

module.exports = Blog;

Next, since our API will have authorisation and authentication features we will also define the structure of a user.

Now, create and user .model.js file in the models directory and add this code:

const mongoose = require('mongoose');

const { Schema } = mongoose;
const { ObjectId } = Schema;

const UserSchema = new Schema({
  id: ObjectId,
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  createdAt: { type: Date },
});


const User = mongoose.model('User', UserSchema);

module.exports = User;

Now, let's take a look at how to use Joi to create validators for our CRUD Blog API. We'll start by installing the library using npm:

npm install joi

Next, let's create a validator for the data being sent to our API when a user creates a new blog post or creates a new account. We'll start by requiring the Joi library and defining our validation schema, create a blog.validator.js and user.validator.js file and respectively add:

/* blog.validator.js file */

const joi = require('joi');

const blogValidator = joi.object({
  title: joi.string()
    .min(5)
    .max(255)
    .required(),

  description: joi.string()
    .min(5)
    .max(255)
    .optional(),

  body: joi.string()
    .min(10)
    .required(),

  state: joi.string(),

  readCount: joi.number(),

  readTime: joi.number(),

  tags: joi.array()
    .optional(),

  createAt: joi.date()
    .default(Date.now()),

  updateAt: joi.date()
    .default(Date.now())
});

const validateBlogMiddleWare = async (req, res, next) => {
  const blogPayload = req.body;
  try {
    await blogValidator.validateAsync(blogPayload);
    next();
  } catch (error) {
    console.log(error);
    return res.status(406).send(error.details[0].message);
  }
}

module.exports = validateBlogMiddleWare;
/* user.validator.js */

const joi = require('joi');

const userValidator = joi.object({
  firstName: joi.string()
    .min(2)
    .max(255)
    .required(),

  lastName: joi.string()
    .min(2)
    .max(255)
    .required(),

  email: joi.string()
    .email({
      minDomainSegments: 2, tlds: {
        allow: ['com', 'net']
      }
    }),

  password: joi.string()
    .pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),

  createAt: joi.date()
    .default(Date.now())
})

const validateUserMiddleWare = async (req, res, next) => {
  const userPayload = req.body;
  try {
    await userValidator.validateAsync(userPayload);
    next();
  } catch (error) {
    console.log(error);
    return res.status(406).send(error.details[0].message);
  }
}

module.exports = validateUserMiddleWare;

Setting Up The Routes

Setting up routes in a Node.js application using the Express router is an essential part of building a CRUD API. The Express router allows you to easily create modular and mountable route handlers, which can help make your code more organized and easier to maintain.

In your routes folder create two files blog.route.js and auth.route.js. To set up routes using the Express router, you will need to require it in your route files. For example in our blog.route.js file:

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

Next, you can define your routes using the various HTTP verbs available in the router object. For example, to create a route that handles GET requests to the '/posts' endpoint, you can use the blogRouter.get() method:

blogRouter.get('/', (req, res) => {
  // Handle the GET request to the '/' endpoint
});

You should also define routes for other HTTP verbs such as POST, PUT, and DELETE using the blogRouter.post(), blogRouter.put(), and blogRouter.delete() methods, respectively. Your blog.route.js file should look like this:

const express = require('express');


const blogRouter = express.Router();

blogRouter.get('/', (req, res) => {
  // Handle the GET request to the '/' endpoint
});


blogRouter.post('/', (req, res) => {
  // Handle the POST request to the '/' endpoint
});

blogRouter.put('/:id', (req, res) => {
  // Handle the PUT request to the '/:id' endpoint
});

blogRouter.delete('/:id', (req, res) => {
  // Handle the DELETE request to the '/' endpoint
});

module.exports = blogRouter;

Same goes for your auth.route.js:

const express = require('express');


const authRouter = express.Router();

authRouter.get('/', (req, res) => {
  // Handle the GET request to the '/' endpoint
});


authRouter.post('/', (req, res) => {
  // Handle the POST request to the '/' endpoint
});

authRouter.put('/:id', (req, res) => {
  // Handle the PUT request to the '/:id' endpoint
});

authRouter.delete('/:id', (req, res) => {
  // Handle the DELETE request to the '/' endpoint
});

module.exports = blogRouter;

Once you have defined your routes, you can mount them to your main Express app using the app.use() method:

This will make all of the routes defined in the router object available at the '/' prefix. For example, the route defined above for handling GET requests to '/' would now be available at '/auth/' and '/blog/'.

const authRouter = require('./src/routes/auth.route');
const userRouter = require('./src/routes/user.route');

const app = express();

app.use('/auth', authRouter);
app.use('/blog', blogRouter);

Implementing The CRUD Functionality For Blog

Since we have our routes set up, it's time to start adding functionality to each of those routes. This is where the real work of building the API happens. In this section, we'll cover how to add the following functionality to your routes:

  • Creating new blog posts

  • Retrieving a list of all blog posts

  • Retrieving a single blog post by ID

  • Updating an existing blog post

  • Deleting a blog post

Now go to controllers folder and create a file named blog.controller.js. The file will hold all the functionality for each route. Next install moment:

npm install moment

Then require this modules in your blog.controller.js file:

const moment = require('moment');
const BlogModel = require('../models/blog.model');
const UserModel = require('../models/user.model');
const readTime = require('../utils/readtime.utils');

We need to create a readTime function that will estimate the amount of time that will be needed to read a blog post.

Now go to the utils folder and create a file named readtime.utils.js and add:

/**
 * readTime - returns minutes required to read text
 * @text: string of words
 *
 * Return: time required to read text in minutes
 */
function readTime(text) {
  const WPM = 150; // Words Per Minute
  const textLength = text.split(' ').length;

  if (textLength > 0) {
    const time = Math.ceil(textLength / WPM);
    return (time);
  }

  return (0);
}

module.exports = readTime;

Let's start by discussing how to create new blog posts.

Creating New Blog Posts

To create a new blog post, you'll need to set up a POST route that accepts data in the request body and uses it to create a new document in your MongoDB collection. Here's an example of what this controller might look like:

async function addBlog(req, res, next) {
  try {
    const { body } = req;

    if (body) {
      const text = body.body;

      body.createdAt = moment().toDate();
      body.updatedAt = moment().toDate();
      body.readTime = readTime(text);
      body.author = `${req.user.firstName} ${req.user.lastName}`;
      body.authorId = req.user._id;

      const blogData = await BlogModel.create(body);

      return res.status(201).json({
        status: true,
        blogData,
      });
    }
  } catch (error) {
    next(error);
  }
}

This route will create a new Blog document using the Blog model and the data sent in the request body. It will then save the new document to the database and return a success message to the client.

Retrieving a List of All Blog Posts

To retrieve a list of all blog posts, you'll need to set up a GET route that queries the database for all documents in the posts collection and returns them to the client. Here's an example of what this controller will look like:

async function getAllBlogs(req, res, next) {
  try {
    const limit = 20;
    let page = 0;

    let blogs = await BlogModel.find()
      .limit(limit)
      .skip(page * limit);

    if (req.query) {
      const {
        pageNumber,
        state,
        author,
        authorId,
        title,
        tag,
        orderBy,
        order,
      } = req.query;

      const findParams = {};
      const sortParams = {};
      let sortOrder;

      if (pageNumber) {
        page = pageNumber - 1;
      }

      // Add find parameters to findParams
      if (authorId) {
        findParams.email = authorId;
      }

      if (state) {
        findParams.state = { $regex: state, $options: 'i' };
      }

      if (author) {
        findParams.author = { $regex: author, $options: 'i' };
      }

      if (title) {
        findParams.title = { $regex: title, $options: 'i' };
      }

      if (tag) {
        findParams.tags = { $elemMatch: { $regex: tag, $options: 'i' } };
      }

      // Add sort parameters to sortParams
      if (orderBy) {
        const sortArray = orderBy.split(',');

        if (order === 'desc') sortOrder = -1;
        else sortOrder = 1;

        for (let i = 0; i < sortArray.length; i += 1) {
          const key = sortArray[i];
          sortParams[key] = sortOrder;
        }
      }

      blogs = await BlogModel.find(findParams)
        .limit(limit)
        .skip(page * limit)
        .sort(sortParams);
    }

    const pageDetails = {};
    const count = await BlogModel.count();

    pageDetails.presentPageNumber = page + 1;
    pageDetails.totalPage = Math.ceil((count / limit));

    return res.status(200).json({
      status: true,
      pageDetails,
      blogs,
    });
  } catch (error) {
    next(error);
  }
}

This route will use the find() method of the Blog model to retrieve all documents in the posts collection. It will then return the documents to the client as a JSON array.

Retrieving a Single Blog Post by ID

To retrieve a single blog post by ID, you'll need to set up a GET route that accepts an ID parameter and uses it to query the database for a single document. Here's an example of what this controller might look like:

async function getBlogById(req, res, next) {
  try {
    const { id } = req.params;

    if (id) {
      const blog = await BlogModel.findById(id);

      if (!blog) {
        return res.status(404).json({
          status: false,
          blog: 'Blog with this id does not exist',
        });
      }

      let { readCount } = blog;
      const { authorId } = blog;

      readCount += 1;

      const updateDetails = { readCount };

      const blogData = await BlogModel.findByIdAndUpdate(id, updateDetails, {
        new: true,
        runValidators: true,
      });

      const author = await UserModel.findById(authorId);
      const authorInfo = {};

      authorInfo.id = author._id;
      authorInfo.firstName = author.firstName;
      authorInfo.lastName = author.lastName;
      authorInfo.email = author.email;

      return res.status(200).json({
        status: true,
        authorInfo,
        blogData,
      });
    }
  } catch (error) {
    next(error);
  }
}

This route will use the findById() method of the Post model to retrieve a single document with the specified ID. It will then return the document to the client as a JSON object.

Updating an Existing Blog Post

To update an existing blog post, you'll need to set up a PUT route that accepts an ID parameter and data in the request body, and uses them to update a single document in the database. Here's an example of what this comtroller:

async function updateBlog(req, res, next) {
  try {
    const { id } = req.params;

    if (id) {
      const updateDetails = req.body;

      const blogId = { _id: id };

      const blog = await BlogModel.findByIdAndUpdate(blogId, updateDetails, {
        new: true,
        runValidators: true,
      });

      return res.status(201).json({
        status: true,
        blog,
      });
    }
  } catch (error) {
    next(error);
  }
}

Deleting a Blog Post

To delete an existing blog post, you'll need to set up a DELETE route that accepts an ID parameter, the ID will be used to delete a single document in the database. Here's an example of what this controller:

async function deleteBlog(req, res, next) {
  try {
    const { id } = req.params;

    if (id) {
      const blog = await BlogModel.deleteOne({ _id: id });

      return res.json({
        status: true,
        blog,
      });
    }
  } catch (error) {
    next(error);
  }
}

We are done writing all the codes for the controllers. Now below the blog.controller.js file, we need to export all the controller modules so that they can be used in blog.route.js file:

module.exports = {
  getAllBlogs,
  getBlogById,
  addBlog,
  updateBlog,
  deleteBlog,
};

We will go to our blog.route.js require all the modules both from our blog.validator.js and blog.controller.js and use the modules in their corresponding routes. Your blog.route.js shoulg look like this:

const express = require('express');
const blogController = require('../controllers/blog.controller');
const blogValidation = require('../validators/blog.validator.js');

const blogRouter = express.Router();

blogRouter.get('/', blogController.getAllBlogs);
blogRouter.get('/:id', blogController.getBlogById);

blogRouter.post(
  '/',
  blogValidation,
  blogController.addBlog,
);

blogRouter.put('/:id', blogController.updateBlog);

blogRouter.delete('/:id', blogController.deleteBlog);

module.exports = blogRouter;

Finally, we will go to our app .js pass our blog router through the app instance as a middleware:

const express = require('express');
const app = express();

app.use('/auth', authRouter);
app.use('/blog', blogRouter);

app.listen(8000, () => {
  console.log('API server listening on port 3000');
});

Authentication and Authorisation

In this section, we will look at how to add authentication and authorisation features to our CRUD blog API using the passport library.

Passport is a popular middleware for authenticating user requests in Node.js applications. It provides a simple and flexible way to authenticate users using a variety of different strategies, such as OAuth, JWT, and many more.

For our API we will use passport for the sign up and login routes, and also we will use to protect some blig routes so that only registered users can access that route.

To use passport, we will first need to install it using npm:

npm install passport

Next, we will need to decide on which authentication strategy we want to use. For the purposes of this tutorial, we will be using the JSON Web Token (JWT) strategy.

To use the JWT strategy, we will need to install the passport-jwt and passport-local library:

npm install passort-jwt passport-local

Next, we will need to configure the JWT strategy by creating a auth.middleware.js file in the middlewares folder. In this file, we will require the passport-jwt library, the jsonwebtoken library, and some other libraries :

const passport = require('passport');
const moment = require('moment');
const LocalStrategy = require('passport-local').Strategy;
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
const UserModel = require('../models/user.model');
require('dotenv').config();

Next, we will need to define our JWT secret and options. The secret is used to encode and decode the JWT, and the options are used to configure the JWT strategy.

// Verify JWT Tokens
passport.use(
  new JWTstrategy(
    {
      secretOrKey: process.env.JWT_SECRET,
      jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
    },
    async (token, done) => {
      try {
        return done(null, token.user);
      } catch (error) {
        done(error);
      }
    },
  ),
);

In this example, we are using the jwtFromRequest function to extract the JWT from the Authorization header in the headers object.

Once we have defined our secret and options, we can create the JWT strategy using the passport.use method for our login and signup route:

passport.use(
  'signup',
  new LocalStrategy(
    {
      usernameField: 'email',
      passwordField: 'password',
      passReqToCallback: true,
    },
    async (req, email, password, done) => {
      const { firstName, lastName } = req.body;
      try {
        const user = await UserModel.create({
          firstName,
          lastName,
          email,
          password,
          createdAt: moment().toDate(),
        });

        return done(null, user);
      } catch (error) {
        done(error);
      }
    },
  ),
);

passport.use(
  'login',
  new LocalStrategy(
    {
      usernameField: 'email',
      passwordField: 'password',
    },
    async (email, password, done) => {
      try {
        const user = await UserModel.findOne({
          email,
        });

        if (!user) {
          return done(null, false, {
            message: 'User not found',
          });
        }

        const validate = await user.isValidPassword(password);

        if (!validate) {
          return done(null, false, {
            message: 'Wrong Password',
          });
        }

        return done(null, user, {
          message: 'Logged in Successfully',
        });
      } catch (error) {
        return done(error);
      }
    },
  ),
);

Now that we have configured the JWT strategy, we can use it to authenticate requests by using the passport.authenticate middleware.

Go to your auth.route.js in the routes folder and add this:

const express = require('express');
const passport = require('passport');
const authController = require('../controllers/auth.controller');
const userValidation = require('../validators/user.validator.js');
require('dotenv').config();

const authRouter = express.Router();

authRouter.post(
  '/signup',
  userValidation,
  passport.authenticate('signup', { session: false }),
  authController.signup,
);

authRouter.post(
  '/login',
  authController.login,
);

module.exports = authRouter;

Finally, we can protect some of our blog routes now:

const express = require('express');
const passport = require('passport');
const blogController = require('../controllers/blog.controller');
const blogValidation = require('../validators/blog.validator.js');

const blogRouter = express.Router();

blogRouter.get('/', blogController.getAllBlogs);
blogRouter.get('/:id', blogController.getBlogById);

blogRouter.post(
  '/',
  passport.authenticate('jwt', { session: false }),
  blogValidation,
  blogController.addBlog,
);

blogRouter.put(
  '/:id',
  passport.authenticate('jwt', { session: false }),
  blogController.updateBlog
);

blogRouter.delete(
  '/:id',
  passport.authenticate('jwt', { session: false }),
  blogController.deleteBlog,
);

module.exports = blogRouter;

Testing The API

It is important to test it to ensure that it is working as expected. One tool that you can use to test your API is Postman.

To test your API with Postman, follow these steps:

  1. Download and install Postman on your computer.

  2. Run Postman and click on the "Import" button in the top-left corner.

  3. In the Import dialog box, select the "Import From Link" tab and paste in the link to your API's base URL. This will import all of the API's endpoints into Postman.

  4. Select an endpoint and click on the "Send" button to send a request to that endpoint. You can also set the HTTP method and add any necessary parameters or body data in the corresponding fields.

  5. Observe the response from the API. It should contain the data that you requested or an error message if something went wrong.

  6. Repeat the process for each endpoint that you want to test.

Testing your API with Postman is a quick and easy way to ensure that your API is functioning properly before you deploy it. It is also a useful tool for debugging any issues that may arise during development.

When testing your endpoints your url will look something like this:

http://localhost:8000/<route>

Below are examples of all the tests carried out on all routes using Postman.

Sign-Up User

  • Route: /auth/signup

  • Method: POST

  • Body:

      {
          "firstName": "David",
          "lastName": "Udo",
          "email": "udodavid46.ud@gmail.com",
          "password": "davidudo"
      }
    
  • Responses

    Success

      {
          "message": "Signup successful",
          "user": {
              "firstName": "David",
              "lastName": "Udo",
              "email": "udodavid46.ud@gmail.com",
              "password": "$2b$10$XRnjWOd7yosf6EfMrMF/h.YaukrNF1.j8WshSDJtaZF6cs.6GyYS.",
              "createdAt": "2022-11-05T04:14:33.837Z",
              "_id": "6365e3290837e1ec02e582ac",
              "__v": 0
          }
      }
    

Login User

  • Route: /auth/login

  • Method: POST

  • Body:

      {
          "email": "udodavid46.ud@gmail.com",
          "password": "davidudo"
      }
    
  • Responses

    Success

      {
          "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjYzNjVlMzI5MDgzN2UxZWMwMmU1ODJhYyIsImVtYWlsIjoidWRvZGF2aWQ0Ni51ZEBnbWFpbC5jb20ifSwiaWF0IjoxNjY3NjIxODQ4fQ.D_-9BccIKmv3w8ExEMMxC7_dDDxO7HNRXgyEesyXf-c"
      }
    

Create Blog

  • Route: /blog

  • Method: POST

  • Header

    • Authorization: Bearer {token
  • Body:

      {
          "title": "JavaScript Tutorial",
          "description": "JavaScript Basics for Beginners",
          "body": "This is the content of the blog.",
          "tags": ["JavaScript", "Beginners"]
      }
    
  • Responses

    Success

      {
          "status": true,
          "blogData": {
            "title": "JavaScript Tutorial",
            "description": "JavaScript Basics for Beginners",
            "author": "David Udo",
            "authorId": "636732cb74766d2d63f2dde2",
            "body": "This is the content of the blog.",
            "state": "draft",
            "readCount": 0,
            "readTime": 1,
            "tags": [
              "JavaScript",
              "Beginners"
            ],
            "createdAt": "2022-11-06T04:19:08.945Z",
            "updatedAt": "2022-11-06T04:19:08.946Z",
            "_id": "636735bc74766d2d63f2ddef",
            "__v": 0
        }
      }
    

Get Blog

  • Route: /blog/:id

  • Method: GET

  • Responses

    Success

      {
          "status": true,
          "authorInfo": {
            "id": "636732cb74766d2d63f2dde2",
            "firstName": "David",
            "lastName": "Udo",
            "email": "udodavid46.ud@gmail.com"
          },
          "blogData": {
            "_id": "636735bc74766d2d63f2ddef",
            "title": "JavaScript Tutorial",
            "description": "JavaScript Basics for Beginners",
            "author": "David Udo",
            "authorId": "636732cb74766d2d63f2dde2",
            "body": "This is the content of the blog.",
            "state": "draft",
            "readCount": 1,
            "readTime": 1,
            "tags": [
              "JavaScript",
              "Beginners"
            ],
            "createdAt": "2022-11-06T04:19:08.945Z",
            "updatedAt": "2022-11-06T04:19:08.946Z",
            "__v": 0
          }
      }
    

Get Blogs

  • Route: /blog

  • Method: GET

  • Query params:

    • pageNumber (default: 1)

    • state (options: draft | published)

    • author

    • authorId

    • title

    • tag (accepts only one tag e.g nodejs)

    • orderBy (e.g createdAt,updatedAt,readTime,readCount)

    • order (options: asc | desc, default: desc)

  • Responses

    Success

      {
          "status": true,
          "pageDetails": {
            "presentPageNumber": 1,
            "totalPage": 1
          },
          "blogs": [
            {
              "_id": "63673ec91963893bbe5cbe89",
              "title": "JavaScript Tutorial",
              "description": "JavaScript Basics for Beginners",
              "author": "David Udo",
              "authorId": "636732cb74766d2d63f2dde2",
              "body": "This is the content of the blog.",
              "state": "draft",
              "readCount": 0,
              "readTime": 1,
              "tags": [
                "JavaScript",
                "Beginners"
              ],
              "createdAt": "2022-11-06T04:57:45.157Z",
              "updatedAt": "2022-11-06T04:57:45.157Z",
              "__v": 0
            },
            {
              "_id": "63673b181963893bbe5cbe7c",
              "title": "NodeJS Tutorial",
              "description": "NodeJS Basics for Beginners",
              "author": "David Udo",
              "authorId": "636732cb74766d2d63f2dde2",
              "body": "This is the content of the blog.",
              "state": "draft",
              "readCount": 0,
              "readTime": 1,
              "tags": [
                "NodeJS",
                "Beginners"
              ],
              "createdAt": "2022-11-06T04:42:00.127Z",
              "updatedAt": "2022-11-06T04:42:00.130Z",
              "__v": 0
            }
          ],
      }
    

Update Blog

  • Route: /orders

  • Method: PUT

  • Header:

    • Authorization: Bearer {token}
  • Responses

    Success

      {
          "status": true,
          "blog": {
            "_id": "636735bc74766d2d63f2ddef",
            "title": "NodeJS Tutorial For Newbies",
            "description": "JavaScript Basics for Beginners",
            "author": "David Udo",
            "authorId": "636732cb74766d2d63f2dde2",
            "body": "This is the content of the blog.",
            "state": "draft",
            "readCount": 1,
            "readTime": 1,
            "tags": [
              "NodeJS",
              "Beginners",
              "JavaScript"
            ],
            "createdAt": "2022-11-06T04:19:08.945Z",
            "updatedAt": "2022-11-06T04:19:08.946Z",
            "__v": 0
          }
      }
    

Delete Blog

  • Route: /orders

  • Method: DELETE

  • Header:

    • Authorization: Bearer {token}
  • Responses

    Success

      {
          "status": true,
          "blog": {
            "acknowledged": true,
            "deletedCount": 1
          }
      }
    

Writing Tests with Jest and Supertest

Jest is a popular JavaScript testing framework that comes with a lot of features out of the box, such as mocking, assertion libraries, and parallel test execution. Supertest is a library that allows us to make HTTP requests to our API and make assertions on the response.

First, let's install the dependencies:

npm install --save-dev jest supertest

Create a directory named tests with the following directory tree:

tests
├── integration
│   ├── auth.route.test.js
│   ├── blog.route.test.js
│   └── user.route.test.js
├── unit
│   └── utils.test.js
└── utils
    └── test.utils.js

Before we start writing our test, we need to export the app instance in our app.js.file, so at the end of the file add:

module.exports = app;

In auth.route.test.js:

const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../../index');
const UserModel = require('../../src/models/user.model');
const { removeAllCollections } = require('../utils/test.utils');

require('dotenv').config();

beforeEach(async () => {
  await mongoose.connect(process.env.MONGODB_TEST_URI);
});

afterEach(async () => {
  await removeAllCollections();
});

afterEach(async () => {
  await mongoose.connection.close();
});

describe('Auth: Signup', () => {
  it('should signup a user', async () => {
    const response = await request(app)
      .post('/auth/signup')
      .set('content-type', 'application/json')
      .send({
        firstName: 'Tobie',
        lastName: 'Augustina',
        email: 'tobi@gmail.com',
        password: '123456',
      });

    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('message');
    expect(response.body).toHaveProperty('user');
    expect(response.body.user).toHaveProperty('firstName', 'Tobie');
    expect(response.body.user).toHaveProperty('lastName', 'Augustina');
    expect(response.body.user).toHaveProperty('email', 'tobi@gmail.com');
  });
});

describe('Auth: Login', () => {
  it('should login a user', async () => {
    // Create user in db
    await UserModel.create({
      firstName: 'Tobie',
      lastName: 'Augustina',
      email: 'tobi@gmail.com',
      password: '123456',
    });

    // Login user
    const response = await request(app)
      .post('/auth/login')
      .set('content-type', 'application/json')
      .send({
        email: 'tobi@gmail.com',
        password: '123456',
      });

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('token');
  });
});

In blog.route.test.js:

const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../../index');
const {
  authoriseUser,
  createBlog,
  removeAllCollections,
} = require('../utils/test.utils');

require('dotenv').config();

beforeEach(async () => {
  await mongoose.connect(process.env.MONGODB_TEST_URI);
});

afterEach(async () => {
  await removeAllCollections();
});

afterEach(async () => {
  await mongoose.connection.close();
});

describe('Blog: Get', () => {
  it('should get all blogs', async () => {
    await createBlog();

    const headerObj = {
      'content-type': 'application/json',
    };

    // Get all blogs
    const response = await request(app).get('/blog').set(headerObj);

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('status', true);
    expect(response.body).toHaveProperty('blogs');
    expect(response.body.blogs[0]).toHaveProperty(
      'title',
      'JavaScript Tutorial',
    );
  });
});

describe('Blog: Get by Id', () => {
  it('should get a single blog', async () => {
    const { blog } = await createBlog();
    const blogId = blog._body.blogData._id;

    const headerObj = {
      'content-type': 'application/json',
    };

    // Get a blog
    const response = await request(app).get(`/blog/${blogId}`).set(headerObj);

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('status', true);
    expect(response.body).toHaveProperty('blogData');
    expect(response.body).toHaveProperty('authorInfo');
    expect(response.body.blogData).toHaveProperty(
      'title',
      'JavaScript Tutorial',
    );
  });
});

describe('Blog: Create Blog', () => {
  it('should create a blog', async () => {
    const user = await authoriseUser();

    const headerObj = {
      'content-type': 'application/json',
      authorization: `Bearer ${user.token}`,
    };

    // Create a blog
    const response = await request(app)
      .post('/blog')
      .set(headerObj)
      .send({
        title: 'JavaScript Tutorial',
        description: 'JavaScript Basics for Beginners',
        body: 'This is the content of the blog.',
        tags: ['JavaScript', 'Beginners'],
      });

    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('status', true);
    expect(response.body).toHaveProperty('blogData');
    expect(response.body.blogData).toHaveProperty(
      'title',
      'JavaScript Tutorial',
    );
  });
});

describe('Blog: Update Blog', () => {
  it('should update a blog', async () => {
    const { blog, user } = await createBlog();
    const blogId = blog._body.blogData._id;

    const headerObj = {
      'content-type': 'application/json',
      authorization: `Bearer ${user.token}`,
    };

    // Update blog
    const response = await request(app)
      .put(`/blog/${blogId}`)
      .set(headerObj)
      .send({
        title: 'JavaScript Tutorial for Beginners',
        state: 'published',
      });

    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('status', true);
    expect(response.body).toHaveProperty('blog');
    expect(response.body.blog).toHaveProperty(
      'title',
      'JavaScript Tutorial for Beginners',
    );
    expect(response.body.blog).toHaveProperty('state', 'published');
  });
});

describe('Blog: Delete Blog', () => {
  it('should delete a blog', async () => {
    const { blog, user } = await createBlog();
    const blogId = blog._body.blogData._id;

    const headerObj = {
      'content-type': 'application/json',
      authorization: `Bearer ${user.token}`,
    };

    // Delete blog
    const response = await request(app)
      .delete(`/blog/${blogId}`)
      .set(headerObj);

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('blog');
    expect(response.body.blog).toHaveProperty('acknowledged', true);
    expect(response.body.blog).toHaveProperty('deletedCount', 1);
  });
});

In utils.test.js:

const readTime = require('../../src/utils/readtime.utils');

describe('Utils', () => {
  const text = 'As with so many technologies, Node.js has its champions and its detractors. But there\'s no denying that it is widely used by some powerhouse websites, including Uber, LinkedIn, and PayPal—which makes it a powerhouse no matter which side of the debate you\'re on. And popular technologies used by big brands are always something to pay attention to when you\'re making career choices. So what is Node.js? Node.js is an open source cross-platform runtime environment written in JavaScript. It is built on Chrome\'s V8 JavaScript engine, which parses and executes the JavaScript code. Node uses an event-driven, non-blocking I/O model, which makes it fast and lightweight. This programming model is one of the main reasons why Node has become so popular. Node is best suited for building software and applications that require real-time, synchronous interactions such as chat apps and websites. Yet it also has other uses and benefits which make it popular among developers, as well, all contributing to its popularity.';

  it('should return the total number of books', () => {
    expect(readTime(text)).toBe(2);
  });
});

In test.utils.js:

const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../../index');

async function authoriseUser() {
  // Signup User
  const user = await request(app)
    .post('/auth/signup')
    .set('content-type', 'application/json')
    .send({
      firstName: 'Obinna',
      lastName: 'Akobundu',
      email: 'obai@gmail.com',
      password: '123456',
    });

  // Login user
  const tokenObj = await request(app)
    .post('/auth/login')
    .set('content-type', 'application/json')
    .send({
      email: 'obai@gmail.com',
      password: '123456',
    });

  const { token } = tokenObj._body;
  const userId = user._body.user._id;

  return {
    token,
    userId,
  };
}

async function createBlog() {
  const user = await authoriseUser();

  const headerObj = {
    'content-type': 'application/json',
    authorization: `Bearer ${user.token}`,
  };

  const blog = await request(app)
    .post('/blog')
    .set(headerObj)
    .send({
      title: 'JavaScript Tutorial',
      description: 'JavaScript Basics for Beginners',
      body: 'This is the content of the blog.',
      tags: ['JavaScript', 'Beginners'],
    });

  return {
    blog,
    user,
  };
}

async function removeAllCollections() {
  const collections = Object.keys(mongoose.connection.collections);
  for (const collectionName of collections) {
    const collection = mongoose.connection.collections[collectionName];
    await collection.deleteMany();
  }
}

module.exports = {
  authoriseUser,
  createBlog,
  removeAllCollections,
};

To run our tests, go to your package.json file and edit the scripts object to look like this:

"scripts": {
    "start": "node index.js",
    "start:dev": "npx nodemon index.js",
    "test": "cross-env NODE_ENV=test jest --detectOpenHandles",
    "seed:db": "node db.seed.js"
  },

Now you can run tests by using the command:

npm run test

Seeding Database

Seeding your database with fake data can be a useful tool for testing and development. It allows you to test your application's functionality without having to manually enter data or rely on a production database.

We will be seeding our database with fake data using faker.js, we first need to install the faker library:

npm install faker

In the root directory create a file named db.seed.js

We will then use the faker library to generate fake data in our database, both for the User and Blog model. We will the use the following code:

const { faker } = require('@faker-js/faker');
const mongoose = require('mongoose');
const UserModel = require('./src/models/user.model');
const BlogModel = require('./src/models/blog.model');
const readTimeFunc = require('./src/utils/readtime.utils');

function randomIntFromInterval(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

async function seedDB() {
  // Connection URL
  const MONGODB_URI = 'mongodb://localhost:27017/blogging_api';

  try {
    await mongoose.connect(MONGODB_URI);

    console.log('Connected correctly to server');

    await UserModel.deleteMany({});
    await BlogModel.deleteMany({});

    // Make a bunch of time series data for user collection
    const usersTimeSeriesData = [];

    for (let i = 0; i < 50; i += 1) {
      const firstName = faker.name.firstName();
      const lastName = faker.name.lastName();
      const email = faker.internet.email(firstName, lastName);
      const password = faker.internet.password();

      const userData = {
        firstName,
        lastName,
        email,
        password,
      };

      usersTimeSeriesData.push(userData);
    }

    const users = await UserModel.insertMany(usersTimeSeriesData);

    // Make a bunch of time series data for blogs collection
    const blogsTimeSeriesData = [];

    for (let i = 0; i < 50; i += 1) {
      const title = faker.lorem.sentence();
      const description = faker.lorem.sentences(3);
      const body = faker.lorem.sentences(20);
      const tags = [];

      for (let j = 0; j < randomIntFromInterval(1, 6); j += 1) {
        const newTag = faker.random.word();
        tags.push(newTag);
      }

      const randomIndex = randomIntFromInterval(0, 49);
      const randomUser = users[randomIndex];

      const stateEnum = ['draft', 'published'];
      const randomState = stateEnum[randomIntFromInterval(0, 1)];

      const randomTime = faker.date.past();

      const blogData = {
        title,
        description,
        authorId: randomUser._id,
        author: `${randomUser.firstName} ${randomUser.lastName}`,
        body,
        state: randomState,
        readTime: readTimeFunc(body),
        tags,
        createdAt: randomTime,
        updatedAt: randomTime,
      };

      blogsTimeSeriesData.push(blogData);
    }

    await BlogModel.insertMany(blogsTimeSeriesData);

    console.log('Database seeded! :)');
  } catch (err) {
    console.log(err.stack);
  }
}

seedDB().then(() => {
  mongoose.connection.close();
});

You can run the db.seed.js file in the root directory:

node db.seed.js

Security

It is important to add security measures to protect the API from unauthorized access and malicious attacks. Here are a few ways you can add security to your API.

In this section will rate limiting, cors, helmet, and other security modules to our API.

  • Rate limiting: Rate limiting is a technique used to control the amount of traffic that can hit your API within a specific time frame. This helps prevent Denial of Service (DoS) attacks and other types of abuse.

  • CORS headers: Cross-Origin Resource Sharing (CORS) headers allow you to specify which domains are allowed to access your API. This helps prevent unauthorized access from other domains.

First of all we need to install express-rate-limiter:

npm install express-rate-limiter

Create a file named rateLimiter.middleware.js in the middleware folder and add:

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

const limiter = rateLimit({
  windowsMs: 0.5 * 60 * 1000, // 15 minutes window
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});

module.exports = limiter;

Edit your app.js file to look like this:

const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');

const authRouter = require('./src/routes/auth.route');
const userRouter = require('./src/routes/user.route');
const blogRouter = require('./src/routes/blog.route');

const limiter = require('./src/middlewares/rateLimiter.middleware');

const connectToMongoDB = require('./src/configs/db.config');
require('dotenv').config();
require('./src/middlewares/auth.middleware');

const { PORT } = process.env;

const app = express();

/* Add Security */
const corsOptions = { origin: '*' };
app.use(cors(corsOptions));

app.use(helmet());
app.disable('x-powered-by');

app.use(limiter);
/* ============ */


process.env.PWD = process.cwd();

// Middlewares
app.use(express.json());
app.use(morgan('tiny'));

// Connect to MongoDB database
connectToMongoDB();


app.use('/auth', authRouter);
app.use('/blog', blogRouter);

// 404
app.use((req, res, next) => {
  res.status(404);
  res.json({
    message: 'Route not found on the server',
  });
  next();
});

// Handle errors.
app.use((err, req, res, next) => {
  // logger.error(err.message);
  console.log(err);
  res.status(err.status || 500);
  res.json({ error: err.message });
  next();
});

app.listen(PORT, () => {
  console.log('Server listening on port, ', PORT);
});

module.exports = app;

Deployment

Cyclic is a powerful and easy-to-use platform for deploying and managing Node.js apps. By connecting your GitHub repository and automating the deployment process, you can save time and focus on building and improving your API.

Before we get started, you'll need to make sure that you have a few things set up:

  1. A GitHub repository with the code for your Node.js API.

  2. A Cyclic account, which you can sign up for on the Cyclic website.

  3. The Cyclic CLI (Command Line Interface) installed on your local machine. You can install the Cyclic CLI by running the following command:

npm install -g @cyclic/cli

Once you have everything set up, you're ready to begin the process of deploying your API on Cyclic. Here are the steps you'll need to follow:

  • Log in to your Cyclic account using the Cyclic CLI:
cyclic login

This will prompt you to enter your email and password, and will authenticate your session.

  • Create a new Cyclic app. You can do this by running the following command:
cyclic create

This will prompt you to enter a name for your app, and will create a new app with that name.

  1. Connect your GitHub repository to your Cyclic app. To do this, you'll need to go to the Settings page for your app on the Cyclic website, and then click the "Connect to GitHub" button. This will prompt you to log in to your GitHub account and authorize Cyclic to access your repositories. Once you've done this, you'll be able to select the repository that contains your Node.js API.

  2. Configure your app's environment variables. These are values that your app needs in order to run properly, but that you don't want to hardcode into your code. For example, if your app needs a database connection string, you can set that as an environment variable so that you can easily change it later if needed. You can configure your app's environment variables on the Settings page for your app on the Cyclic website.

  3. Deploy your app to Cyclic. Once you've connected your GitHub repository and configured your environment variables, you're ready to deploy your app. To do this, simply go to the Deployments page for your app on the Cyclic website, and click the "Deploy" button. This will start the process of building and deploying your app.

  4. Once the deployment process is completed, you should be able to access your API on the URL provided by cyclic.

That's it! You've successfully deployed your Node.js API on Cyclic through GitHub. With this setup, Cyclic will automatically deploy your API whenever you push changes to your GitHub repository. In case you made any changes to the environment variables,you need to redeploy the app for the changes to take effect.

Conclusion

In conclusion, building a CRUD blog API using Node.js, Express.js, and Mongoose is a straightforward process. By following the steps outlined in this tutorial, you should be able to build a functional API that allows you to create, read, update, and delete blog posts.

One key aspect to keep in mind is the proper use of HTTP verbs and endpoints to adhere to RESTful conventions. Properly structuring your routes will make your API more intuitive and easier to use for developers who may consume it.

Additionally, using Mongoose to interact with a MongoDB database allows you to easily create and manipulate data while also providing powerful validation and middleware functionality.

Overall, building a CRUD API with these technologies is a great way to get started with building APIs with Node.js and can serve as the foundation for more complex projects.