Blog implementation using Node.js Express: Altschool project

Blog implementation using Node.js Express: Altschool project

Introduction

In this article, we are going to be covering the full implementation of the REST API BLOG PROJECT from start to finish. Expect loads of concepts to be uncovered and links to external resources will be provided as we proceed throughout the article. Let's Get Started!

Project Brief

This overview covers the requirements for this project and includes the following

  1. Users should have a first_name, last_name, email, and password, (you can add other attributes you want to store about the user)

  2. A user should be able to sign up and sign in to the blog app

  3. Use JWT as an authentication strategy and expire the token after 1 hour

  4. A blog can be in two states; draft and published

  5. Logged-in and not logged in users should be able to get a list of published blogs created

  6. Logged-in and not logged in users should be able to get a published blog

  7. When a blog is created, it is in a draft state

  8. The owner of the blog should be able to update the state of the blog to a published blog

  9. The owner of a blog should be able to edit the blog in draft or published state

  10. The owner of the blog should be able to delete the blog in draft or published state

  11. The owner of the blog should be able to get a list of their blogs.

    1. The endpoint should be paginated

    2. It should be filterable by state

  12. Blogs created should have title, description, tags, author, timestamp, state, read_count, reading_time and body

  13. The list of blogs endpoint that can be accessed by both logged-in and not logged in users should be paginated,

    1. default it to 20 blogs per page.

    2. It should also be searchable by author, title and tags.

    3. It should also be orderable by read_count, reading_time and timestamp

  14. When a single blog is requested, the API should return the user information(the author) with the blog. The read_count of the blog too should be updated by 1

  15. Come up with an algorithm for calculating the reading time of the blog.

  16. Write tests for all endpoints

Prerequisites

Database

MongoDB Atlas

Runtime

Node JS

Web Framework

Express JS

API Platform

Postman

Project Build

In this session, we will go through the structure of the project(Npm packages to install and folder/file structuring), creating the server, the database setup, the models, the controllers, the routes, the middleware, the testing, and finally hosting to the server.

Project Structure

We start by creating a new folder to store all program files which we will call "Altschool Blog Exam Project". Once created we navigate to a terminal(bash/zsh for Mac OS and Linux, git bash/cmd for windows) and type the command npm init -y to initialize a package.json file.

The Npm packages needed to install include the following

  • express (HTTP Server)

  • express-rate-limit (Security Package)

  • bcryptJs (Password hashing and validator)

  • mongoose (To implement the Mongo DB)

  • dotenv (Load the .env file)

  • jsonwebtoken (Auth Strategy)

  • passport (Auth Strategy)

  • passport-JWT (Auth Strategy)

  • passport-Local (Auth Strategy)

  • cors (Security Package)

  • helmet (Security Package)

  • moment (Date and time validator)

  • xss-clean (Security Package)

  • joi (Data Validator)

  • jest (API test)

  • supertest (API test)

  • mongodb-memory-server (Creating Mongo Database for testing)

We also have the development dependencies

  • nodemon (Helps restart server when changes are made)

To install the packages in the dependencies, input the following in the terminal

npm install bcryptjs dotenv jest supertest joi xss-clean express express-rate-limiter mongodb-memory-server moment cors jsonwebtoken mongoose passport passport-local passport-jwt jsonwebtoken helmet

To install the development dependencies, input the following in the terminal

npm install --save-dev nodemon

Some packages would be better installed globally e.g nodemon given the size of the package, this will give access at any time when u create another project folder.

To install a package globally, input the following in the terminal

npm install -g nodemon

Now we go on with our folders and file structuring. We should have a proper folder setup containing our program files to enable us to navigate easily and give us cleaner code and better code readability.

Here we go with a step-by-step representation of how we should structure our folders and files

NOTE: This sample revolves around the current project but is still a good baseline to implement for other projects, should you build in the future. The folder and file naming are general samples but you could still name the folder as you like.

  • controllers: folder

    • blogController.js

    • userController.js

  • db

    • connect.js
  • middleware: folder

    • passport.js
  • models : folder

    • blogModel.js

    • userModel.js

  • node_modules(created automatically as it contains packages installed)

  • routes: folder

    • blogRoutes.js

    • userRoutes.js

  • test: folder

    • blog.spec.js

    • blog.test.data.js

    • database.js

    • home.spec.js

    • user.spec.js

  • .env: file

  • .gitignore: file

  • app.js: file

  • index.js: file

  • package-lock.json: file

  • Procfile: file

  • ReadME: file

Database Setup

As we know from the folder structuring session, we have our db folder and this is where we set up the database connection. This would require us to "require" the mongoose npm package. The setup will look like this:

const mongoose = require("mongoose");

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGO_URL);
    console.log(`Connected to Database: ${conn.connection.host} `);
  } catch (error) {
    console.error(`Error: ${error} `);
    process.exit(1);
  }
};

module.exports = connectDB;

In the scenario above, I'm using the MongoDB Atlas but you can also use the local installation and it would look like this:

const mongoose = require("mongoose");

const connectDB = (url) => {
  return mongoose
    .connect(url)
    .then((con) => console.log(`Connected to Database: ${con.connection.host}`))
    .catch((err) => console.log(err));
};

module.exports = connectDB;

Now for the first setup to work(given we using the atlas), we have to configure our .env file which we conveniently created earlier and it will go like this:

MONGO_URL=<your connection string goes here>

HTTP Server Setup

In this session, we need to create our server and also initiate our MongoDB connection that we implemented earlier in the db file. The code will be executed in the app.js file and will look like this:

require("dotenv").config();

// Express
const express = require("express");
const app = express();

// Database
const connectDB = require("./db/connect");
connectDB();

// Server
const port = process.env.PORT || 5000;

const start = async () => {
  try {
    app.listen(port, () => {
      console.log(`Server is listening on port ${port}...`);
    });
  } catch (error) {
    console.log(error);
  }
};

start();

To check that server and database have been executed, we'll make use of the terminal. But I would like to note that I start my terminal nodemon(our server package) using the command npm start and this can be configured in the package.json file by inputting the following:

"scripts": {
    "start": "node app.js"
  }

The above will automatically start the app.js file server with the command "npm start". After that is done, if the code was executed properly, the result on the terminal should look like this:

NOTE: It's possible to come across errors if there was a likely error in the setup of the code base both in the app.js or the connect.js file. However, if the steps above were followed correctly, it's very unlikely.

Data Models Setup

Next, we define our user and blog models.

Using our models' folder and the userModel.js/blogModel.js file we can simply implement our model data structure that Mongo documents will contain.

We will be going through each implementation of the model in a list method:

  • User Model

    • Code implementation:

        const mongoose = require("mongoose");
        const bcrypt = require("bcryptjs");
      
        const Schema = mongoose.Schema;
      
        const UserSchema = new Schema({
          firstName: {
            type: String,
            required: [true, "Please Enter First Name"],
            maxlength: 50,
            minlength: 3,
          },
          lastName: {
            type: String,
            required: [true, "Please Enter First Name"],
            maxlength: 50,
            minlength: 3,
          },
          email: {
            type: String,
            required: [true, "Please Enter  Email Address"],
            unique: true,
          },
          password: {
            type: String,
            required: [true, "Please enter password"],
            minlength: [6, "Password must be at least 6 characters"],
          },
          blogs: [
            {
              type: Schema.Types.ObjectId,
              ref: "Blog",
            },
          ],
        });
      
        UserSchema.pre("save", async function (next) {
          if (!this.isModified("password")) return;
          const salt = await bcrypt.genSalt(10);
          this.password = await bcrypt.hash(this.password, salt);
          next();
        });
      
        UserSchema.methods.comparePassword = async function (candidatePassword) {
          const isMatch = await bcrypt.compare(candidatePassword, this.password);
          return isMatch;
        };
      
        const userModel = mongoose.model("User", UserSchema);
      
        module.exports = { userModel };
      

      First, we defined the packages required which included mongoose to help us with the documentation of the MongoDB Atlas and the bcryptjs to help us with the hashing of the user data password and validate the user password for safe use.

      Next, we defined the schema as the "UserSchema" which we used to create a series of information that the user should pass into the API platform in postman which will be documented in the MongoDB(we'll show the full process as you go on with the article).

      Next, we make use of our bcrypt by executing both the hashing of the passwords and the password validation using the "bcrypt.hash" and the "bcrypt.comparePassword" method

      Finally, we export our "userModel".

  • Blog Model

    • Code implementation:

        const mongoose = require("mongoose");
        const Schema = mongoose.Schema;
      
        const BlogSchema = new Schema(
          {
            title: {
              type: String,
              required: true,
              unique: true,
            },
            description: {
              type: String,
            },
            author: {
              type: Schema.ObjectId,
              ref: "User",
            },
            state: {
              type: String,
              required: true,
              enum: ["draft", "published"],
              default: "draft",
            },
            read_count: {
              type: Number,
              default: 0,
            },
            reading_time: {
              type: String,
              required: true,
              default: "0s",
            },
            tags: [String],
            body: {
              type: String,
              required: true,
            },
          },
          { timestamps: true }
        );
      
        const blogModel = mongoose.model("Blog", BlogSchema);
      
        module.exports = { blogModel };
      

      This is much like the user model defined before with a couple of exceptions being no use of the bcryptjs. The information structure also differs from the user model as this model's information includes what is needed to create the blog article that will be documented in MongoDB.

The Controller

Next, we define our routes controller. This folder helps us implement code instructions in each given route using the CRUD(Create Read Update Delete) method.

We will be going through each implementation of the model in a list method:

  • User Controller

    • Code implementation:

      First, we have the imports...

        const jwt = require("jsonwebtoken");
        const { userModel } = require("../models/user");
        const { blogModel } = require("../models/blog");
      
        require("dotenv").config();
      

      Next, we have the controller routes implementation...

        const signUpUser = async (req, res) => {
          const user = await userModel
            .findOne({ email: req.user.email })
            .select("-password");
      
          user.firstname = req.body.firstName;
          user.lastname = req.body.lastName;
          user.email = req.body.email;
      
          await user.save();
      
          res.status(201).json({
            message: "Signup successful",
            user: user,
          });
        };
      
        const signInUser = (req, res, { err, user, info }) => {
          if (!user) {
            return res.json({ message: "Username or password is incorrect" });
          }
      
          req.login(user, { session: false }, async (error) => {
            if (error) return res.status(400).json(error);
      
            const body = {
              _id: user._id,
              firstName: user.firstName,
              lastName: user.lastName,
              email: user.email,
            };
      
            const token = jwt.sign(
              { user: body },
              process.env.JWT_SECRET || "something_secret",
              {
                expiresIn: process.env.JWT_LIFETIME || "1h",
              }
            );
      
            return res.status(200).json({ token });
          });
        };
      
        const getUserBlogs = async (req, res) => {
          const user = await userModel
            .findOne({ _id: req.params.id })
            .select("-password");
      
          if (!user) {
            return res
              .status(404)
              .json({ message: `No user with id : ${req.params.id}` });
          }
      
          const page = Number(req.query.page) || 1;
          const limit = Number(req.query.limit) || 20;
          const skip = (page - 1) * limit;
      
          const state = req.query.state;
      
          var blogs = await blogModel.find({}).skip(skip).limit(limit);
      
          if (state) {
            var blogs = await blogModel.find({ state: state }).skip(skip).limit(limit);
          }
      
          res.status(200).json({ blogs });
        };
      

      Finally, we export the routes...

        module.exports = {
          signUpUser,
          signInUser,
          getUserBlogs,
        };
      

Blog Controller

  • Code implementation:

    First, the imports...

      const { blogModel } = require("../models/blog");
      const { userModel } = require("../models/user");
      const moment = require("moment");
    

    Next, we have the controller routes implementation...

      const getAllBlogs = async (req, res) => {
        const page = Number(req.query.page) || 1;
        const limit = Number(req.query.limit) || 20;
        const skip = (page - 1) * limit;
    
        const author = req.query.author;
        const title = req.query.title;
        const tags = req.query.tags;
    
        var blogs = await blogModel.find({}).skip(skip).limit(limit);
    
        if (author) {
          var blogs = await blogModel.find({ author: author });
        }
    
        if (title) {
          var blogs = await blogModel.find({ title: title });
        }
    
        if (tags) {
          var blogs = await blogModel.find({ tags: tags });
        }
    
        res.status(200).json({ blogs });
      };
    
      const getAllBlogsByOrder = async (req, res) => {
        const read_count = req.query.read_count;
        const reading_time = req.query.reading_time;
        const timestamps = req.query.timestamps;
    
        if (read_count === "ascending") {
          const ascending = await blogModel
            .find({ state: "published" })
            .sort({ read_count: 1 })
            .skip(skip)
            .limit(limit);
    
          return res.status(200).json({
            blogs: { ascending },
            message: "read_count is in ascending order",
          });
        } else if (read_count === "descending") {
          const descending = await blogModel
            .find({ state: "published" })
            .sort({ read_count: -1 })
            .skip(skip)
            .limit(limit);
    
          return res.status(200).json({
            blogs: { descending },
            message: "read_count is in descending order",
          });
        }
    
        if (reading_time === "ascending") {
          const ascending = await blogModel
            .find({ state: "published" })
            .sort({ reading_time: 1 })
            .skip(skip)
            .limit(limit);
    
          return res.status(200).json({
            blogs: { ascending },
            message: "reading_time is in ascending order",
          });
        } else if (reading_time === "descending") {
          const descending = await blogModel
            .find({ state: "published" })
            .sort({ reading_time: -1 })
            .skip(skip)
            .limit(limit);
    
          return res.status(200).json({
            blogs: { descending },
            message: "reading_time is in descending order",
          });
        }
    
        if (timestamps === "ascending") {
          const ascending = await blogModel
            .find({ state: "published" })
            .sort({ timestamps: 1 })
            .skip(skip)
            .limit(limit);
    
          return res.status(200).json({
            blogs: { ascending },
            message: "timestamps is in ascending order",
          });
        } else if (timestamps === "descending") {
          const descending = await blogModel
            .find({ state: "published" })
            .sort({ timestamps: -1 })
            .skip(skip)
            .limit(limit);
    
          return res.status(200).json({
            blogs: { descending },
            message: "timestamps is in descending order",
          });
        }
    
        const descending = await blogModel
          .find({ state: "published" })
          .sort({ read_count: -1 })
          .skip(skip)
          .limit(limit);
    
        res.status(200).json({
          blogs: { descending },
          message: "Most read blogs",
        });
      };
    
      const createBlogs = async (req, res) => {
        const body = req.body.body;
        const wpm = 225;
        const words = body.trim().split(/\s+/).length;
        const time = Math.ceil(words / wpm + 1);
    
        const blogData = {
          title: req.body.title,
          description: req.body.description,
          tags: req.body.tags,
          author: req.user._id,
          body: body,
          // Default/lowest time span with this calc is 2mins
          reading_time: `${time}mins`,
          timestamps: moment().toDate(),
        };
    
        const blog = await blogModel.create(blogData);
    
        const user = await userModel.findById(req.user._id);
    
        user.blogs.push(blog._id);
    
        await user.save();
    
        res.status(200).json({ blog });
      };
    
      const getSingleBlog = async (req, res) => {
        const { id: blogId } = req.params;
        const blog = await blogModel
          .findOne({ _id: blogId })
          .populate("author", "-password")
    
        if (!blog) {
          res.status(404).json({ message: `No Blog with id: ${blogId} ` });
        }
    
        blog.read_count++;
    
        await blog.save();
    
        res.status(200).json({ blog });
      };
    
      const publishBlog = async (req, res) => {
        const { id: blogId } = req.params;
        const blog = await blogModel.findOne({ _id: blogId });
    
        if (!blog) {
          return res.status(404).json({ message: `No Blog with id: ${blogId} ` });
        }
    
        blog.state = req.body.state;
    
        await blog.save();
    
        res.status(200).json({ blog });
      };
    
      const updateBlog = async (req, res) => {
        const { id: blogId } = req.params;
        const blog = await blogModel.findOne({ _id: blogId });
    
        if (!blog) {
          res.status(404).json({ message: `No Blog with id: ${blogId} ` });
        }
    
        (blog.title = req.body.title),
          (blog.description = req.body.description),
          (blog.tags = req.body.tags),
          (blog.body = req.body.body),
          (blog.state = req.body.state);
        await blog.save();
    
        res.status(200).json({ blog });
      };
    
      const deleteBlog = async (req, res) => {
        const { id: blogId } = req.params;
        const blog = await blogModel.findOneAndDelete({ _id: blogId });
    
        if (!blog) {
          return res.status(404).json({ message: `No Blog with id: ${blogId} ` });
        }
    
        await blog.remove();
    
        res.status(200).json({ message: "Blog has been sucessfully deleted" });
      };
    

    Finally, the exports...

      module.exports = {
        getAllBlogs,
        getAllBlogsByOrder,
        createBlogs,
        getSingleBlog,
        publishBlog,
        updateBlog,
        deleteBlog,
      };
    

Middleware

This contains the passport.js file where we implement the Passport JWT Authentication Strategy. This strategy helps secure user data by signing up and signing in to get tokens for authorization. This token has an expiry of 1h though before resetting where the user will have to sign in again.

The code implementation...

const passport = require("passport");
const localStrategy = require("passport-local").Strategy;
const { userModel } = require("../models/user");

const JWTstrategy = require("passport-jwt").Strategy;
const ExtractJWT = require("passport-jwt").ExtractJwt;

passport.use(
  new JWTstrategy(
    {
      secretOrKey: process.env.JWT_SECRET || "something_secret",
      // jwtFromRequest: ExtractJWT.fromUrlQueryParameter("secret_token"),
      jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
    },
    async (token, done) => {
      try {
        return done(null, token.user);
      } catch (error) {
        done(error);
      }
    }
  )
);

passport.use(
  "signup",
  new localStrategy(
    {
      usernameField: "email",
      passwordField: "password",
      passReqToCallback: true,
    },
    async (req, username, password, done) => {
      const email = req.body.email;
      const firstName = req.body.firstName;
      const lastName = req.body.lastName;
      try {
        const user = await userModel.create({
          firstName,
          lastName,
          email,
          password,
        });

        return done(null, user, { message: "User created successfully" });
      } catch (error) {
        done(error);
      }
    }
  )
);

passport.use(
  "login",
  new localStrategy(
    {
      usernameField: "email",
      passwordField: "password",
      passReqToCallback: true,
    },
    async (req, username, password, done) => {
      const email = req.body.email;
      const firstName = req.body.firstName;
      const lastName = req.body.lastName;
      try {
        const user = await userModel.findOne({ email, firstName, lastName });

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

        const validate = await user.comparePassword(password);

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

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

Routes

The routes evolve around setting up HTTP Requests and they include the GET, POST, PATCH/PUT and DELETE

The GET Request is used to request data from a specified resource on websites.

The POST Request sends data to a server to create a resource.

The PATCH/PUT Request sends data to a server to update a resource.

The DELETE Request sends data to a server to delete a resource.

We will be going through each implementation of the model in a list method:

  • User Routes

    • Code implementation:

      First, the imports and executing the "Router()"...

        const express = require("express");
        const passport = require("passport");
        const router = express.Router();
      

      Next, call the routes from the controller by importing the folder and file using "require()"...

        const { signUpUser, signInUser, getUserBlogs } = require("../controllers/user");
      

      Next, we use the ".route()" to implement HTTP requests GET and POST...

        router.post(
          "/signup",
          passport.authenticate("signup", { session: false }),
          signUpUser
        );
        router.post("/signin", async (req, res, next) =>
          passport.authenticate("login", (err, user, info) => {
            signInUser(req, res, { err, user, info });
          })(req, res, next)
        );
      
        router.get(
          "/:id",
          passport.authenticate("jwt", { session: false }),
          getUserBlogs
        );
      

      Finally, we export...

        module.exports = router;
      
  • Blog Routes

  • Code implementation:

    First, the imports and executing the "Router()"...

      const express = require("express");
      const passport = require("passport");
      const router = express.Router();
    

    Next, call the routes from the controller by importing the folder and file using "require()"...

      const {
        getAllBlogs,
        getAllBlogsByOrder,
        getSingleBlog,
        createBlogs,
        publishBlog,
        updateBlog,
        deleteBlog,
      } = require("../controllers/blog");
    

    Next, we use the ".route()" to implement HTTP requests GET POST PATCH and DELETE...

      router
        .route("/")
        .get(getAllBlogs)
        .post(passport.authenticate("jwt", { session: false }), createBlogs);
    
      router.get("/order", getAllBlogsByOrder);
    
      router
        .route("/:id")
        .get(getSingleBlog)
        .patch(passport.authenticate("jwt", { session: false }), publishBlog)
        .put(passport.authenticate("jwt", { session: false }), updateBlog)
        .delete(passport.authenticate("jwt", { session: false }), deleteBlog);
    

    Finally, we export...

      module.exports = router;
    

    Test

    Alright, next we have the testing of our API endpoints. This is important for ensuring that your API performs as expected when faced with a wide variety of expected and unexpected requests.

    We will be going through each implementation of the model in a list method:

    • Blog test

      • blog.spec.js

        • Code implementation:

          First, the imports...

            const request = require("supertest");
            const { connect } = require("./database");
            const app = require("../index");
            const moment = require("moment");
            const { blogModel } = require("../models/blog");
            const BlogsTest = require("./blog.test.data");
            const { userModel } = require("../models/user");
          

          Next, we test all the API endpoints...

            describe("Blog Route", () => {
              let conn;
              let token;
          
              beforeAll(async () => {
                conn = await connect();
          
                await userModel.create({
                  firstName: "bliss",
                  lastName: "felix",
                  email: "blissfelix@gmail.com",
                  password: "password",
                  state: "draft",
                });
          
                const signInResponse = await request(app)
                  .post("/user/signin")
                  .set("content-type", "application/json")
                  .send({
                    firstName: "bliss",
                    lastName: "felix",
                    email: "blissfelix@gmail.com",
                    password: "password",
                  });
          
                token = signInResponse.body.token;
              });
          
              beforeEach(async () => {
                for (const blogTest of BlogsTest) {
                  const blogData = new blogModel({
                    _id: blogTest._id,
                    body: blogTest.body,
                    title: blogTest.title,
                    description: blogTest.description,
                    tags: blogTest.tags,
                    state: blogTest.state,
                  });
                  await blogData.save();
                }
              });
          
              afterEach(async () => {
                await conn.cleanup();
              });
          
              afterAll(async () => {
                await conn.disconnect();
              });
          
              it("should return created blogs", async () => {
                const res = await request(app)
                  .post("/blog")
                  .set("content-type", "application/json")
                  .set("Authorization", `Bearer ${token}`)
                  .send({
                    body: "rat are amazing for real????",
                    title: "My wonderful rat",
                    tags: "@rat",
                    description: "my desc",
                    timestamps: moment().toDate(),
                    reading_time: "2min",
                  });
          
                expect(res.status).toBe(200);
                expect(res.body).toHaveProperty("blog");
              });
          
              it("should return all blogs", async () => {
                const res = await request(app)
                  .get("/blog")
                  .set("Accept", "application/json")
                  .expect("Content-Type", /json/);
          
                expect(res.status).toBe(200);
                expect(res.body).toHaveProperty("blogs");
              });
          
              it("should return single blog", async () => {
                const blog = await blogModel.findOne();
          
                const res = await request(app)
                  .get(`/blog/${blog._id}`)
                  .set("content-type", "application/json")
                  .set("Authorization", `Bearer ${token}`);
          
                expect(res.status).toBe(200);
              });
          
              it("should publish blog state", async () => {
                const blog = await blogModel.findOne();
          
                const res = await request(app)
                  .patch(`/blog/${blog._id}`)
                  .set("Accept", "application/json")
                  .set("Authorization", `Bearer ${token}`)
                  .send({
                    state: "published",
                  });
          
                expect(res.status).toBe(200);
                expect(res.body).toHaveProperty("blog");
              });
          
              it("should update blog", async () => {
                const blog = await blogModel.findOne();
          
                const res = await request(app)
                  .put(`/blog/${blog._id}`)
                  .set("content-type", "application/json")
                  .set("Authorization", `Bearer ${token}`)
                  .send({
                    body: "new body hello",
                    title: "new title relax",
                    description: "new desc",
                    tags: "@new",
                    state: "published",
                  });
          
                expect(res.status).toBe(200);
                expect(res.body).toHaveProperty("blog");
              });
          
              it("should delete post", async () => {
                const blog = await blogModel.findOne();
                const res = await request(app)
                  .delete(`/blog/${blog._id}`)
                  .set("content-type", "application/json")
                  .set("Authorization", `Bearer ${token}`);
          
                expect(res.status).toBe(200);
                expect(res.body).toHaveProperty("message");
              });
            });
          
    • blog.test.data.js

      • Code implementation:

        Here we code sample data for the blog as a blueprint...

          const mongoose = require("mongoose");
          const blogs = [
            {
              _id: mongoose.Types.ObjectId(),
              body: "Hello world, my name is Bliss and im from Altschool Africa",
              title: "Hello",
              description: "My desc",
              tags: "@Altschool",
              state: "published",
            },
          ];
        
          module.exports = blogs;
        
  • Database

    • database.js

      • Code implementation:

        Here we set up a sample MongoDB connection server for testing...

          const mongoose = require("mongoose");
          const { MongoMemoryServer } = require("mongodb-memory-server");
        
          mongoose.Promise = global.Promise;
        
          class Connection {
            constructor() {
              this.mongoServer = MongoMemoryServer.create();
              this.connection = null;
            }
        
            async connect() {
              this.mongoServer = await MongoMemoryServer.create();
              const mongoUri = this.mongoServer.getUri();
        
              this.connection = await mongoose.connect(mongoUri, {
                useNewUrlParser: true,
                useUnifiedTopology: true,
              });
            }
        
            async disconnect() {
              await mongoose.disconnect();
              await this.mongoServer.stop();
            }
        
            async cleanup() {
              const models = Object.keys(this.connection.models);
              const promises = [];
        
              models.map((model) => {
                promises.push(this.connection.models[model].deleteMany({}));
              });
        
              await Promise.all(promises);
            }
          }
        
          /**
           * Create the initial database connection.
           *
           * @async
           * @return {Promise<Object>}
           */
          exports.connect = async () => {
            const conn = new Connection();
            await conn.connect();
            return conn;
          };
        
  • User Test

    • user.spec.js

      First, the imports...

        const { connect } = require("./database");
        const request = require("supertest");
        const app = require("../index");
        const { blogModel } = require("../models/blog");
        const BlogsTest = require("./blog.test.data");
        const { userModel } = require("../models/user");
        const mongoose = require("mongoose");
      

      Next, we test all API endpoints...

        describe("User: POST /user/signup", () => {
          let conn;
      
          beforeAll(async () => {
            conn = await connect();
          });
      
          afterEach(async () => {
            await conn.cleanup();
          });
      
          afterAll(async () => {
            await conn.disconnect();
          });
      
          it("should signup a user", async () => {
            const user = {
              firstName: "bliss",
              lastName: "felix",
              email: "blissfelix@gmail.com",
              password: "password",
            };
            const res = await request(app)
              .post("/user/signup")
              .set("content-type", "application/json")
              .send(user);
      
            expect(res.status).toBe(201);
            expect(res.body).toHaveProperty("message");
            expect(res.body).toHaveProperty("user");
            expect(res.body.user).toHaveProperty("firstName", "bliss");
            expect(res.body.user).toHaveProperty("lastName", "felix");
            expect(res.body.user).toHaveProperty("email", "blissfelix@gmail.com");
          });
        });
      
        describe("User: POST /user/signin", () => {
          let conn;
      
          beforeAll(async () => {
            conn = await connect();
          });
      
          afterEach(async () => {
            await conn.cleanup();
          });
      
          afterAll(async () => {
            await conn.disconnect();
          });
      
          it("should signin a user", async () => {
            const user = {
              firstName: "bliss",
              lastName: "felix",
              email: "blissfelix@gmail.com",
              password: "password",
            };
      
            await userModel.create(user);
      
            const res = await request(app)
              .post("/user/signin")
              .set("content-type", "application/json")
              .send(user);
      
            expect(res.status).toBe(200);
            expect(res.body).toHaveProperty("token");
          });
        });
      
  • Home Route Test

    • home.spec.js

      Here we test the sample home route and 404 route created in the index.js...

        describe("Home Route", () => {
          it("Should return status true", async () => {
            const res = await request(app)
              .get("/")
              .set("content-type", "application/json");
            expect(res.status).toBe(200);
            expect(res.body).toEqual({ message: "successful" });
          });
      
          it("Should return error when routed to undefined route", async () => {
            const res = await request(app)
              .get("/undefined")
              .set("content-type", "application/json");
            expect(res.status).toBe(404);
            expect(res.body).toEqual({ message: "route not found" });
          });
        });
      

The Final File For All Endpoints

Earlier we looked at the app.js where we implemented the server and database connection leaving us to the final file to define...the index.js file.

This code base will be where all endpoints meet, it involves the implementation of every middleware, security, routes to the sample home and error routes for testing.

The code shows the full and final implementation...

const dotenv = require("dotenv");
dotenv.config();
require("./middleware/passport");

// extra security packages for Heroku
const helmet = require("helmet");
const cors = require("cors");
const xss = require("xss-clean");
const rateLimiter = require("express-rate-limit");

const express = require("express");

const app = express();

app.set("trust proxy", 1);
app.use(
  rateLimiter({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
  })
);
app.use(express.json());
app.use(helmet());
app.use(cors());
app.use(xss());

// Routes
const userRoute = require("./routes/user");
const blogRoute = require("./routes/blog");

// Calling routes
app.use("/blog", blogRoute);
app.use("/user", userRoute);

// Home route
app.get("/", (req, res) => {
  return res.status(200).json({ message: "successful" });
});

// 404 route
app.use("*", (req, res) => {
  return res.status(404).json({ message: "route not found" });
});

module.exports = app;

Conclusion

Okay! That's it for this article. Hope you guys found it helpful and enjoyed it.

If you have any questions feel free to drop a comment and if you liked the article and found it useful in any way do drop a like, would be very much appreciated. Thank you very much.

Did you find this article valuable?

Support Bliss Abhademere by becoming a sponsor. Any amount is appreciated!