Restful API for a Blog Service with NodeJS Express, MongoDB and Mongoose

Table of contents

Introduction

In this tutorial article, we will create a simple blog that blog visitors can read blog posts available. This allows us to explore the operations that are common to almost every blog application, retrieving article content from a database.

Here, this article shows how you can create a “Dynamic Blog” using Nodejs, Express and MongoDB and some other libraries, which you can then populate with routes, models, controllers, and database calls using mongo cloud. In this case, we’ll use the tool to create the framework for our sample blog application, to which we’ll add all the other code step by step needed by the blog. The creating site is extremely simple, requiring only that you invoke the generator on the command line interface with a new project name. We would be working on a simple CRUD application.

This API should be able to:

  1. Signup a user

  2. Login a user

  3. A user must be able to create a post

  4. A user can update his/her post

  5. A user can get all published posts

  6. A user can also 6 delete a post either in draft or published state

Blog Setup

  1. Create an empty folder for your project

  2. Create or initialize your NodeJS project

    • Open an empty folder in the VScode (or any code editor || IDE)

    • Enter npm init in the open terminal

    • Follow through with the prompt messages in the terminal to set up a new nodejs project. (You can enter app.js/server.js in the prompt for the entry point).

    • In your terminal, enter the code below to install the package

npm i express nodemon mongoose dotenv jsonwebtoken body-parser

we’d be installing more packages as we progress with the blogging app, after we’ve Successfully installed all the necessary packages, we proceed to set up files and folders to get our project running.

  1. We are going to create a .env file for environmental variables.
PORT = 4000
MONGODB_URL=mongodb+srv://tobisam:test1234@cluster0.5i3n4iw.mongodb.net/?retryWrites=true&w=majority 
JWT_SECRET=5ehnnd'[e]ebedm'ldfinfrvhbf79t8u]7%$5dh`
  1. A server.js file (our application entry set up)

     const express = require("express");
     require("dotenv").config();
     const bodyParser = require("body-parser");
    
  2. Creating our database:

    install mongoose, a javascript package for MongoDB querying, npm i mongoose

     const mongoose = require("mongoose");
    
     const MONGODB_URL = process.env.MONGODB_URL;
    
     // connect to mongodb
    
     function connectToMongoDB() {
       mongoose.connect(MONGODB_URL);
    
       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 };
    
  3. Model

    This folder houses our database schemas.

    Since we are using MongoDB for this project it is necessary to note that MongoDB stores data in JSON (object) format, unlike the regular table in traditional databases.

    So the schema describes how the fields are stored in the database.

    We are going to be creating some files inside the model's folder

    • user.js

      install bcrypt for password hashing npm i bcrypt

      install a validator to validate your field entries in the schema npm i validator

        const mongoose = require("mongoose");
        const bcrypt = require("bcrypt");
        const validator = require("validator");
      
        const { Schema } = mongoose;
      
        userSchema = new Schema({
          first_name: {
            type: String,
            required: true
          },
      
          last_name: {
            type: String,
            required: true
          },
      
          email: {
            type: String,
            required: true,
            unique: [true, "email already existed, please try another mail"]
          },
      
          password: {
            type: String,
            required: true
          }
        });
      
        // static signup method
        userSchema.statics.signup = async function(
          email,
          password,
          first_name,
          last_name
        ) {
          // validation
          if (!email || !password) {
            throw Error("All fields must be filled");
          }
      
          if (!validator.isEmail(email)) {
            throw Error("Email is not valid");
          }
      
          if (!validator.isStrongPassword(password)) {
            throw Error("Password is not strong enough!");
          }
      
          const exists = await this.findOne({ email });
          if (exists) {
            throw Error("Email already in use!");
          }
      
          const salt = await bcrypt.genSalt(10);
          const hash = await bcrypt.hash(password, salt);
      
          const user = await this.create({
            email: email,
            password: hash,
            first_name,
            last_name
          });
          return user;
        };
      
        // static login method
        userSchema.statics.login = async function(email, password) {
          // validation
          if (!email || !password) {
            throw Error("All fields must be filled");
          }
      
          const user = await this.findOne({ email });
          if (!user) {
            throw Error("Incorrect Email!");
          }
      
          const match = await bcrypt.compare(password, user.password);
          if (!match) {
            throw Error("Incorrect password!");
          }
      
          return user;
        };
      
        const userModel = mongoose.model("User", userSchema);
      
        module.exports = userModel;
      
      • install mongoosePaginate, a javascript package that handles pagination, npm i mongoose-paginate-v2
    ```javascript
    const mongoose = require("mongoose");
    const mongoosePaginate = require("mongoose-paginate-v2");

    const Schema = mongoose.Schema;

    const blogSchema = new Schema(
      {
        title: {
          type: String,
          required: true,
          unique: [true, "title must be unique"]
        },

        description: {
          type: String
        },

        body: {
          type: String,
          required: true
        },

        author: {
          type: String
        },

        read_count: {
          type: Number,
          default: 0
        },

        reading_time: {
          type: String
        },

        tags: {
          type: String,
          enum: ["draft", "published"]
        }
      },
      {
        timestamps: true
      }
    );

    blogSchema.plugin(mongoosePaginate);

    const blogModel = mongoose.model("Blog", blogSchema);

    module.exports = blogModel;
    ```
  1. CONTROLLERS, MIDDLEWARE AND ROUTERS

    1. CONTROLLER

      Controllers are responsible for handling incoming requests and sending back responses or it could also mean a function written to manipulate data. We are going to be creating a controllers folder in our project directory

      • USER CONTROLLER

          const User = require("../models/users");
          const jwt = require("jsonwebtoken");
        
          const createToken = (_id, email) => {
            return jwt.sign({ id: _id, email: email }, process.env.JWT_SECRET, {
              expiresIn: "1h"
            });
          };
        
          // login user
          const loginUser = async (req, res) => {
            const { email, password } = req.body;
        
            try {
              const user = await User.login(email, password);
        
              // createToken
              const token = createToken(user._id, email);
        
              res.status(200).json({ message: "Login successful", email, token });
            } catch (error) {
              res.status(404).json({ error: error.message });
            }
          };
        
          // signup user
          const signupUser = async (req, res) => {
            const { email, password, first_name, last_name } = req.body;
        
            try {
              const user = await User.signup(email, password, first_name, last_name);
        
              // create token
              const token = createToken(user._id, email);
        
              res.status(200).json({
                message: "Signup was successful",
                user: { email, first_name, last_name, token }
              });
            } catch (error) {
              res.status(400).json({ error: error.message });
            }
          };
        
          module.exports = { loginUser, signupUser };
        
      • BLOG CONTROLLER

          const mongoose = require("mongoose");
        
          const blogModel = require("../models/blogs");
          const readingTime = require("reading-time");
        
          // GET all blogs
        
          const updateOptionForSorting = require("../helpers/updateOptions");
        
          const getAllBlogs = async (req, res) => {
            try {
              let { page, sortBy, tags, author, title } = req.query;
        
              const options = {
                limit: 20,
                collation: {
                  locale: "en"
                },
                lean: true
              };
        
              let query = {};
              console.log(page);
        
              if (page === undefined) {
                page = 1;
              }
              page = Number(page);
        
              if (!(typeof page === "number")) {
                return res.status(400).json({ error: "invalid page number specified" });
              }
              options.page = page;
        
              if (tags !== undefined) {
                if (tags !== "draft" && tags !== "published") {
                  return res.status(400).json({ error: "invalid tags specified" });
                }
        
                query.tags = tags;
              }
        
              if (author !== undefined) {
                query.author = author;
              }
        
              if (title !== undefined) {
                query.title = title;
              }
        
              if (sortBy !== undefined) {
                if (
                  (sortBy !== "timestamp") &
                  (sortBy !== "read-count") &
                  (sortBy !== "read-time")
                ) {
                  return res.status(400).json({ error: "invalid sortBy parameter" });
                }
                updateOptionForSorting(sortBy, options);
              }
        
              console.log(options);
              console.log(query);
        
              const paginatedBlogs = await blogModel.paginate(query, options);
              return res.status(200).json({ data: paginatedBlogs.docs });
            } catch (error) {
              res.status(404).json({ error: error.message });
            }
          };
        
          // GET a single blog
          const getABlog = async (req, res) => {
            const { id } = req.params;
        
            if (!mongoose.Types.ObjectId.isValid(id)) {
              return res.status(404).json({ error: "No such blog" });
            }
        
            try {
              const blog = await blogModel.findById(id);
        
              let initialCount = blog.read_count;
              const newCount = initialCount + 1;
        
              await blogModel.updateOne({ read_count: newCount });
        
              if (!blog) {
                return res.status(404).json({ error: "No such blog" });
              }
        
              res.status(200).json(blog);
            } catch (error) {
              res.status(404).json({ error: error.message });
            }
          };
        
          // CREATE a new blog
          const createBlog = async (req, res) => {
            const { title, description, body, author, tags } = req.body;
        
            const stats = readingTime(body);
            const { text } = stats;
        
            try {
              const blog = await blogModel.create({
                title,
                description,
                body,
                author,
                tags,
                reading_time: text
              });
              res.status(200).json(blog);
            } catch (err) {
              res.status(400).json({ error: err.message });
            }
          };
        
          // UPDATE a blog
          const updateBlog = async (req, res) => {
            const { id } = req.params;
            const { body } = req;
        
            if (!mongoose.Types.ObjectId.isValid(id)) {
              return res.status(404).json({ error: "No such blog" });
            }
        
            try {
              const blog = await blogModel.findByIdAndUpdate({ _id: id }, { ...body });
        
              if (!blog) {
                return res.status(404).json({ error: "No such blog" });
              }
        
              res.status(200).json(blog);
            } catch (error) {
              res.status(404).json({ error: err.message });
            }
          };
        
          // DELETE a blog
          const deleteBlog = async (req, res) => {
            const { id } = req.params;
        
            if (!mongoose.Types.ObjectId.isValid(id)) {
              return res.status(404).json({ error: "No such blog" });
            }
        
            try {
              const blog = await blogModel.findByIdAndDelete({ _id: id });
        
              if (!blog) {
                return res.status(404).json({ error: "No such blog" });
              }
        
              res.status(200).json(blog);
            } catch (error) {
              res.status(404).json({ error: err.message });
            }
          };
        
          module.exports = {
            getAllBlogs,
            getABlog,
            createBlog,
            updateBlog,
            deleteBlog
          };
        
    2. ROUTES

      • USER ROUTES

          const express = require("express");
        
          const userRouter = express.Router();
        
          const { loginUser, signupUser } = require("../controllers/users");
        
          // login route
          userRouter.post("/login", loginUser);
        
          // signup route
          userRouter.post("/signup", signupUser);
        
          module.exports = userRouter;
        
      • BLOG ROUTES

          const express = require("express");
          const blogRouter = express.Router();
          const authenticate = require("../middlewares/authenticate");
        
          const {
            getAllBlogs,
            getABlog,
            createBlog,
            updateBlog,
            deleteBlog
          } = require("../controllers/blogs");
        
          // GET all Blogs
          blogRouter.get("/", getAllBlogs);
        
          // GET a Blog
          blogRouter.get("/:id", getABlog);
        
          // CREATE a Blogs
          blogRouter.post("/", authenticate, createBlog);
        
          // UPDATE all Blogs
          blogRouter.patch("/:id", authenticate, updateBlog);
        
          // DELETE all Blogs
          blogRouter.delete("/:id", authenticate, deleteBlog);
        
          module.exports = blogRouter;
        
    3. MIDDLEWARE

      • AUTHENTICATE

          const jwt = require("jsonwebtoken");
        
          const authenticate = async (req, res, next) => {
            let token = req.get("Authorization");
        
            if (token === null || token === undefined) {
              return res.status(403).json({ error: "forbidden" });
            }
        
            token = req.get("Authorization").replace("Bearer ", "").trim();
        
            try {
              const userDetails = await jwt.verify(token, process.env.JWT_SECRET);
            } catch (error) {
              return res.status(401).json({ error: "not authorized" });
            }
        
            next();
          };
        
          module.exports = authenticate;
        

        I hope you found the article useful, you can check out the full project repository on GitHub: https://github.com/tobisamcode