Creating a blog API with Node.js, Express.js and MongoDB

Creating a blog API with Node.js, Express.js and MongoDB

A step by step process to building a blogging API

Photo by Fotis Fotopoulos on Unsplash

In this article, we will build a blog API step by step, from initialization to implementation and testing. The functionalities of this blog API will include CRUD (i.e. Creating a blog post, Reading a blog post, Updating a blog post and Deleting a blog post), Validation(user validation and blog validation), and Testing.

Prerequisite

Knowledge of Node.js, Express.js and MongoDB

Setting Up

  1. To start this project, you have to create a new folder on your desktop that will include your js files and folders subsequently

  2. Set up a Database

    • create two MongoDB databases (main and test)on your local MongoDB server (MongoDB compass) or in the cloud (MongoDB Atlas)

    • copy the connection string and paste it into your environment variable (.env) file.

  3. Create a package.JSON file, run the node package manager npm init and install the dependencies from the root directory by doing npm install <module name>.

  4. Create a .env file which is the environment variable to keep some critical info like USER_PASSWORD, MongoDB URL and some other vital info

  5. Install nodemon npm install nodemon to keep your app running after every saves. For example, run nodemon server.js

After setting up our project locally, then we start creating files and folders

Note: Kindly create a .gitignore file to keep files like node_module and .env untracked by Git.

Here's a list of some essential files;

  • app.js

  • server.js

  • blogdb.js

  • blog_model.js

  • user_model.js

  • auth_router.js

  • user_router.js

  • blog_router.js

  • user_controller.js

  • blogValidation.js

  • userValidation.js

app.js

The app.js is the base of our code, where we must import some inbuilt modules like express, passport, rate limiter, etc., as required in our code. It is also necessary to add the routes, middlewares and global error handling. Here's what my app.js look like

const express = require("express");
const passport = require("passport");
const helmet = require("helmet");
const rateLimiter = require("express-rate-limit");

const authRouter = require("./routes/auth_router");
const blogRoute = require("./routes/blog_router");
const userRoute = require("./routes/user_router");
require("./middleware/passport");

const app = express();

// Rate Limitter
const limiter = rateLimiter({
  windowMs: 1 * 60 * 1000,
  max: 4,
  standardHeaders: true,
  legacyHeaders: false,
});

// middleware
app.use(passport.initialize());
app.use(express.json());
app.use(express.urlencoded());
app.use(helmet);
app.use(limiter);

app.use("/", authRouter);
app.use("/blogs", blogRoute);
app.use("/users", userRoute);
app.get("/", (req, res) => {
  res.send("welcome to homepage");
});
app.use((err, req, res, next) => {
  const status = err.status || 500;
  const message = err.message.message || " error in server";
  return res.status(status).json({ status: "something went wrong", message });
});

module.exports = app;

server.js

This is where our server runs by listening to our PORT from our environment variable file, and then we console.log() the callback function to be sure that the server is properly running. So the purpose of having our server running separately is that when we are testing our endpoints when our server is running, we can also use the same server for testing.

require("dotenv").config();
const connectToMongoDB = require("./blogdb");
const app = require("./app");
const PORT = process.env.PORT || 4000
// const localHost = "127.0.0.1"
connectToMongoDB();

app.listen(PORT, () => {
  console.log("Listening on port", PORT);
});

blogdb.js

Here we require mongoose and our environment variable file to use the CONNECTION_URL, so on connection, we have a callback function that outputs a success message, and if there's an error, we also console it and then export it to use in our app.

const mongoose = require('mongoose');
require('dotenv').config();
const CONNECTION_URL = process.env.pizza_Connection_Url


mongoose.connect(CONNECTION_URL)

function connectToMongodb(){

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

mongoose.connection.on("error", (err) => {
    console.log("An error occurred while connecting to MongoDB");
    console.log(err);
});

}
module.exports = connectToMongodb

MODELS

After ensuring our app is running perfectly and connected to the database, we can now create models for both the blog and the user. In these files, we have our mongoose, brycpt and validator required.

blog_model.js

const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const ObjectId = mongoose.Schema.ObjectId;


const blogSchema = new Schema({
  title: {
    type: String,
    required: true,
  },

  description: {
    type: String,
    trim: true,
  },


  author_id: {
    type: ObjectId,
    ref: "user",
  },

  state: {
    type: String,
    enum: ["published", "draft"],
    default: "draft",
  },

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

  reading_time: { type: String },

  tags: [String],

  body: {
    type: String,
    trim: true,
    required: [true, "you blog must have a body"],
  },
  timestamp: {
    type: Date,
    default: Date.now(),
  },
});

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

module.exports = Blog;

user_model.js

const bcrypt = require("bcrypt");
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;

const userSchema = new Schema({
  email: {
    type: String,
    lowercase: true,
    require: [true, " A user must have a email"],
    trim: true,
    unique: true,
  },

  first_name: {
    type: String,
    minLength: 2,
    require: true,
  },

  last_name: {
    type: String,
    minLength: 2,
    require: true,
  },

  password: {
    type: String,
    minLength: 2,
    require: true,
    trim: true,
    require: [true, "kindly enter your password"],
  },
  createdAt: {
    type: Date,
    default: Date.now(),
  },
});

userSchema.pre("save", async function (next) {
  // Only run this function if password was actually modified
  if (!this.isModified("password")) return next();

  this.password = await bcrypt.hash(this.password, 12);

  // Delete passwordConfirm field
  this.confirmPassword = undefined;
  next();
});

userSchema.methods.correctPassword = async function (
  candidatePassword,
  userPassword
) {
  return await bcrypt.compare(candidatePassword, userPassword);
};

const user = mongoose.model("user", userSchema);
module.exports = user;

CONTROLLERS

In this folder, we are going to create two files

blogController.js

In the blogController.js file, we import the blogModel and other various functions like createBlog,getAllBlogs, getBlogById, updateBlogs, and DeleteBlog

const blogModel = require("../model/blog_model");

exports.createBlog = async function (req, res, next) {
  try {
    const blogBody = req.body;
    const blogReadingTime = function () {
      const blogLenght =
        blogBody.body.split(" ").length +
        blogBody.title.split(" ").length +
        blogBody.description.split(" ").length;
      console.log(blogLenght);
      const totalReadingtime = blogLenght / 200;
      return `${totalReadingtime} minute`;
    };
    console.log(req.user)
    // console.log({...req.body, author: req.user._id })
    blogBody.reading_time = blogReadingTime();
    let newBlog = await  blogModel.create({ ...req.body,author_id:req.user._id })
   console.log(newBlog)
    return res.status(201).json({
      status: "success",
      newBlog,
    });
  } catch (error) {
    next(error);
  }
};
exports.getAllBlogs = async function (req, res, next) {
  try {
    const filterBlog = { state: "published" };
    const queryObj = { ...req.query };
    // console.log(queryObj);
    let blogQuery = blogModel.find(filterBlog);

    //sorting read_count, reading_time and timestamp
    if (req.query.sort) {
      const searchBy = req.query.sort.split(",").join(" ");
      blogQuery = blogQuery.sort(searchBy);
    } else {
      blogQuery.sort();
    }

    //pagination
    const page = +req.query.page || 1;
    const limit = +req.query.limit || 20;
    const skip = (page - 1) * limit;
    blogQuery.skip(skip).limit(limit);
    const Blogs = await blogQuery;
    res.status(200).json({ status: "success", blogList: Blogs.length, Blogs });
  } catch (error) {
    next(error);
  }
};
exports.ownerBlog = async function (req, res, next) {
  console.log(req.user);
  try {
    const Blog = await blogModel.find({ author: req.user._id });
    console.log(Blog);
    return res.status(200).json({ status: "success", Blog });
  } catch (error) {
    next(error);
  }
};
exports.getBlogById = async function (req, res, next) {
  try {
    const { blogId } = req.params;
    console.log(blogId);
    // console.log(blogId);

    let Blog = await blogModel.findById(blogId).populate("author_id");
    console.log(Blog);
    if (!Blog) {
      res.status(404);
      const error = new Error("Can't find blog with this ID");
      return next(error);
    }
    Blog.read_count += 1;
    await Blog.save({ validateBeforeSave: true });

    res.status(200).json({ status: "success", blogList: Blog.length, Blog });
  } catch (error) {
    next(error);
  }
};
exports.updateBlog = async function (req, res, next) {
  try {
    const { blogId } = req.params;
    const { state } = req.body;

    const blogToBeUpadated = await blogModel.findById(blogId);
    console.log(blogToBeUpadated)
    if (!blogToBeUpadated) {
      return next(new Error("Sorry, this is not your blog"));
    }
    console.log(blogToBeUpadated.author_id,req.user._id)
    if (blogToBeUpadated.author_id.toString() !== req.user._id) {
      return next(new Error("Sorry, you are not authorized to update this blog "));
    }

    const updatedBlog = await blogModel.findByIdAndUpdate(blogId, state, {
      new: true,
      runValidators: true,
    });
    console.log(updatedBlog);
    if (!updatedBlog) {
      res.status(404);
      const error = new Error("Can't find blog with this ID");
      next(error);
    }
      console.log(updatedBlog.author_id,req.user._id)
    if (updatedBlog.author_id.toString() !== req.user._id) {
      return next(
        new Error("Sorry, you are not authorized to update this blog")
      );
    }
    updatedBlog.state = state;
    await updatedBlog.save();
    return res.status(200).json({ status: "success", updatedBlog });
  } catch (error) {
    next(error);
  }
};

exports.deleteblog = async function (req, res, next) {
  try {
    const { blogId } = req.params;
    const blogToBeDeleted = await blogModel.findById(blogId);
    if (!blogToBeDeleted) {
      return next(new Error("Sorry,this is not your blog"));
    }
    if (blogToBeDeleted.author_id.toString() !== req.user._id) {
      return next(new Error("Sorry, you are not authorized to delete this blog "));
    }
    const Blog = await blogModel.findByIdAndDelete(blogId);
    console.log(Blog);
    if (!Blog) {
      res.status(404);
      const error = new Error("Can't find blog with this ID");
      return next(error);
    }
    return res.status(204).json({ messsge: "deleted successfully" });
  } catch (error) {
    next(error);
  }
};

userController.js

In the userController, we also import similar functions like getAllUser, getUserById, updateUser, deleteUser and the userModel.

const userModel = require("../model/user_model");

exports.getAllUsers = async function (req, res, next) {
  try {
    const Users = await userModel.find();
    return res
      .status(200)
      .json({ message: "success", result: Users.length, Users });
  } catch (error) {
    next(error);
  }
};

exports.getUserById = async function (req, res, ) {
  try {
    const { userId } = req.params;
    console.log(userId);
    const user = await userModel.findById(userId);
    console.log(user);
    if (!user) {
      return res.status(404).json({ status: false, order: null });
    }

    return res.json({ status: true, user });
  } catch (error) {
    return res.status(404).json({ message: "Sorry, can't get user by id", error });
  }
};

exports.updateUser = async function (req, res, ) {
  try {
    const { userId } = req.params;
    console.log (userId);
    const userDetails = req.body;

    const user = userModel.findById(userId);
    console.log(user);
    if (!user) {
      return res.status(404).json({ status: false, order: null });
    }
    await userModel.findByIdAndUpdate(userId, userDetails, {
      runValidators: true, // Runs validations for the updated fields
    });

    return res.json({ status: true });
  } catch (error) {
   return res.status(404).json({ message: "user update is unsuccessful", error });
  }
};

exports.deleteUser = async function (req, res, ) {
  try {
    const { userId } = req.params;
    // console.log (userId);

    const user = await userModel.findById(userId);
    console.log(user._id);
    if (!user) {
      return res.status(404).json({ status: false, order: null });
    }
    // console.log(user);
    await userModel.deleteOne(user, {

      runValidators: true, // Runs validations for the updated fields
    });

    return res.json({ status: true });
  } catch (error) {
   return res.status(404).json({ message: "user update is unsuccessful", error });
  }
};

VALIDATORS

Apart from using mongoose for validating our user input, we can also use the third-party library called JOI to validate user input. JOI has a way of sending a friendly error message to the user when user inputs are wrong. You can check more on JOI here. In this folder, we have two files.

blogValidation.js

const joi = require("joi");

const blogValidationMiddleware = async (req, res, next) => {
  try {
    await blogSchema.validateAsync(req.body);
    next();
  } catch (error) {
    console.log("err", error);
    return res.status(406).send(error.details[0].message);
  }
};
const blogSchema = joi.object({
  title: joi.string().min(2).max(30).required(),
  description: joi.string().min(10).max(250).optional().trim(),
  author: joi.ref("user"),
  state: joi.array().has(["published,draft"]).default("draft"),
  read_count: joi.number().default(0),
  reading_time: joi.string(),
  tags: joi.string(),
  body: joi.string().trim().required(),
  timestamp: joi.date().default(Date.now),
});

module.exports = blogValidationMiddleware;

userValidation.js

const joi = require("joi");
joiObjectId = require("joi-objectid")(joi);

const userValidatorMiddleWear = async (req, res, next) => {
  try {
    userValidator.validateAsync(req.user);
    next();
  } catch (error) {
    res.status(406).send(error.details[0].message);
  }
};

const userValidator = joi.object({
  email: joi
    .string()
    .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } })
    .required(),
  first_name: joi.string().min(2).required(),
  last_name: joi.string().min(2).required(),
  password: joi.string().min(2).trim().required(),
  confirmPassword: joi.ref("password"),
});

module.exports = userValidatorMiddleWear;

MIDDLEWARE

In this folder, we have the passport.js file where we will require JSON web token, passport, passport-jwt and our userModel. The JWT strategy gets user input and signs by sending a token to the user. We also have the signup and sign-in function where the user is authenticated using the JSON web token strategy to sign user input.

const passport = require('passport');
const jwt = require('jsonwebtoken');
const userModel = require('../model/user_model');


const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
require('dotenv').config();

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



// SIGNUP CONTROLLER ROUTE USING THE FORM_ ENCODED

//sign token function
const signToken = (user) => {
    return jwt.sign({ user }, process.env.JWT_SECRET, {
      expiresIn: process.env.EXPIRATION_TIME,
    });
  };
  exports.signup = async function (req, res, next) {
    try {
      const { email, first_name, last_name, password } = req.body;
      const user = await userModel.create({
        email,
        first_name,
        last_name,
        password,
      });
      user.password = undefined;
      const token = signToken(user);
      return res.json({
        message: "Signup successfull",
        user,
        token,
      });
    } catch (error) {
      next(error);
    }
  };


  exports.login = async function (req, res, next) {
      try {
        const { email,password } = req.body;

        if (!email || !password ) {
          const error = new Error("Username or password is incorrect");
          return next(error);
        }
        // check if user exist && password is correct

        const user = await userModel.findOne({ email });
        console.log(user);
        if (!user || !(await user.correctPassword(password, user.password))) {
          const error = new Error("Incorrect email or password");
          return next(error);
        }

        // if all the conditions are meet send token
        user.password = undefined;
        const token = signToken(user);
        res.status(200).json({
          status: "success",
          token,
        });
      } catch (error) {
        return next(error, { message: "login was not successful try Again!!!" });
      }
    };

TEST

Testing our endpoints is the last thing we do after building our API to ensure that our app is working correctly, as we have variables for our API in our .env file. We also have dummy variables and a dummy server for testing our API. In our test folder, we have two files; blog.test.js and user.test.js. For testing our endpoints, we will be using jest and supertest to run our test, so we have to install jest and supertest i.e. npm install jest supertest After installing it then, we require the supertest in our test files.

blog.test.js

Here we will require our blogModel, supertest, app, mongoose and dummy variables from our .env in our blog.test.js. We will connect to our test database to test our API. To make our testing more effortless, it is better to test for the response given by each endpoint.

const { JsonWebTokenError } = require("jsonwebtoken");
const mongoose = require("mongoose");
const supertest = require("supertest");
require("dotenv").config();
 const blog_test_url = process.env.blog_test_url;
const app = require("../app");
const TEST_TOKEN = process.env.TEST_TOKEN;


jest.setTimeout(40000);

beforeAll((done) => {
  mongoose.connect(blog_test_url);
  mongoose.connection.on("connected", async () => {
    console.log("connected to Mongodb successfully");
  });

  mongoose.connection.on("error", (err) => {
    console.log(err);
    console.log("An error occured");
  });
  done();
});
afterAll((done) => {
  mongoose.connection.close(() => done());
});
// //BLOG ROUTE TESTING


let blogId
test("POST blog",async()=>{

    const blog_body = {
        title: "adeola's blog",
        tags: "lifestyle",
        body: "my new  blog",
        author: "adeola",
        description: "my first blog",
      };

      const response = await supertest(app)
        .post("/blogs")
        .set("Authorization", `Bearer ${TEST_TOKEN}`)
        .send(blog_body);
      blogId = response.body.newBlog._id;
      expect(response.status).toBe(201);
      expect(response.body.newBlog.title).toBe("adeola's blog");
      expect(response.body.newBlog.state).toBe("draft");
})


test("GET blog",async()=>{

      const response = await supertest(app)
        .get("/blogs")
        .set("authrization", `Bearer ${TEST_TOKEN}`)

      expect(response.status).toBe(200);
      expect(response.body.status).toBe("success")

})

test("GET blog?id",async()=>{

    const response = await supertest(app)
      .get(`/blogs/${blogId}`)
      .set("Authorization", `Bearer ${TEST_TOKEN}`);
    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty("Blog");
})

user.test.js

const mongoose = require("mongoose");
const supertest = require("supertest");
require("dotenv").config();
 const blog_test_url = process.env.blog_test_url;
const app = require("../app");
const TEST_TOKEN = process.env.TEST_TOKEN;

jest.setTimeout(40000);


beforeAll((done) => {
  mongoose.connect(blog_test_url);
  mongoose.connection.on("connected", async () => {
    console.log("connected to Mongodb successfully");
  });

  mongoose.connection.on("error", (err) => {
    console.log(err);
    console.log("An error occured");
  });
  done();
});
afterAll((done) => {
  mongoose.connection.close(() => done());
});


let userId
test("POST user",async()=>{

            const userData = {
              first_name: "adeola",
              last_name: "aderemi",
              email: "demo@gmail.com",
              password: "qwerty",
            };
            const response = await supertest(app)
              .post("/signup")
              .set("content-type", "application/x-www-form-urlencoded")
              .send(userData);

              userId = response.body.user._id;
              console.log(userId);
            expect(response.status).toBe(200);
            expect(response.body.message).toBe("Signup successfull");
            expect(response.body).toHaveProperty("user");
})
test("GET user",async()=>{


            const response = await supertest(app)
              .get("/users")
              .set("Authorization", `Bearer ${TEST_TOKEN}`);
            expect(response.status).toBe(200);
            expect(response.body.message).toBe("success");
})

test("get userByID",async()=>{

  const response = await supertest(app)
    .get(`/users/${userId}`)
    .set("Authorization", `Bearer ${TEST_TOKEN}`)

  expect(response.status).toBe(200);
  expect(response.body.status).toBe(JSON.parse("true"));
  expect(response.body).toHaveProperty("user")
})

test("update userByID",async()=>{
  const userData= {
    email: "adeola@gmail.come",
  };
  const response = await supertest(app)
    .patch(`/users/${userId}`)
    .set("Authorization", `Bearer ${TEST_TOKEN}`)
    .send(userData);

  expect(response.status).toBe(200);
  expect(response.body.status).toBe(JSON.parse("true"));
})


test("delete userByID",async()=>{
  const response = await supertest(app)
  .delete(`/users/${userId}`)
  .set("Authorization", `Bearer ${TEST_TOKEN}`);

expect(response.status).toBe(200);
expect(response.body.status).toBe(JSON.parse("true"));
})

Now that our API is functional, we can push it to GitHub, and you can add more functionalities later if you like. Thank you, and I hope you found this article beneficial. Kindly drop a comment if you have any feedback.


P.S. This is my first technical article. I'll do better. You can check out my other writeups here and connect with me here. Thank you again.