Creating a Blogging App API with Nodejs

ยท

14 min read

Hello there, I'm going to take you through implementing a simple blogging app API with Nodejs in this article. We will learn how to use the basic CRUD operations effectively to create a seamless backend application. We will cover a lot of new concepts alongside.

About Project

At the end of this article, our application should be able to do the following:

  • Register new users

  • Log in users to their account

  • Creation of new blog post (only logged-in users can do this)

  • Delete and Update the blog

  • Allow users to read blog posts without requiring a login

Prerequisites

Before going ahead with this article, you should have basic knowledge of the following for a better understanding:

Project Setup

Let us start by initializing a new Nodejs project. First, we initialize a new package.json file with npm

npm init -y

Then we install the following packages with npm to get running:

npm i express bcrypt cors dotenv joi jsonwebtoken mongoose moment 
npm i -D nodemon

Below is a run-through of the following packages installed:

  • express (HTTP server)

  • bcrypt (hashing / encrypting user's passwords before saving them to the database)

  • cors (for handling)

  • joi (schema validation)

  • jsonwebtoken (user authorization)

  • mongoose (for interacting with MongoDB database)

  • moment (for formatting dates)

Building our Application

To get started we will create an new file app.js in the root directory. Add the following lines of code there:

app.js

const express = require("express");;
const cors = require("cors")
require("dotenv").config();

const app = express();
app.use(cors())
app.use(express.json());

app.get("/health", (req, res) => {
  res.send("App working fine ๐Ÿ‘");
});


const PORT = process.env.PORT || 3000;
connectDB();

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

Above we get our app running by defining a listening port, setting up cors and express.json() for parsing json objects. We also created a get request at /health which we can run at http://localhost:3000 to check if our app is running fine, before doing that run the command below in your terminal to get the app started.

nodemon app.js

After running this command, you should get a Listening on port, 3000 message on your terminal.

Setting up the App Database

We need to create a connection to our MongoDB database in order to interact with it, we will be connecting to a local MongoDB server on our system.

Create a folder db in the root directory and add the file index.js in it. Add the following to the index.js file

const mongoose = require("mongoose");
require("dotenv").config()
const connectDB = () => {
  mongoose.connect(process.env.MONGODB_URI);

  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 = { connectDB };

Above we load a MongoDB URI from the .env file and create a connection on it. To create your MongoDB connection uri, create a .env in the root directory and add this environmental variable to it:

MONGODB_URI="mongodb://127.0.0.1:27017/blog"

After re-saving your index.js file, you should get a Connected to MongoDB Successfully message on your terminal.

Defining our Application Models

We need to create MongoDB data models with mongoose to help define a structure to which we will be accepting data to it.

To get started, create a models folder in the root directory and add the following files blog.js and user.js in it. Add the following lines of code to the respective files

blog.js

const mongoose = require("mongoose");

const blogSchema = mongoose.Schema({
  title: {
    type: String,
    required: true,
    unique: true,
  },
  description: {
    type: String,
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "User",
  },
  state: {
    type: String,
    enum: ["draft", "published"],
  },
  read_count: {
    type: Number,
  },
  reading_time: {
    type: Number,
  },
  tags: [{ type: String }],
  body: {
    type: String,
    required: true,
  },
  created_at: { type: Date, required: true, default: Date.now },
});

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

module.exports = BlogModel;

Above, we use mongoose to initialize a schema which we use to define our models. We define the various fields we will need for our blog data model. You can read more on mongoose models here. The author field which might look particulary different because it contains a ref field is defined that way so that we can save and get the author data without having to pass the whole author data object, instead we will pass only an id and when we want to get the author information, we do that through population in mongoose. You can read more on mongoose population here.

user.js

const mongoose = require("mongoose");
const bcrypt = require("bcrypt");

const userSchema = mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
    min: [6, "Password too short"],
  },
  first_name: {
    type: String,
  },
  last_name: {
    type: String,
  },
});

userSchema.pre("save", async function (next) {
  const hash = await bcrypt.hash(this.password, 10);
  this.password = hash;
  next();
});

userSchema.methods.isValidPassword = async function (password) {
  const user = this;
  const compare = await bcrypt.compare(password, user.password);

  return compare;
};

const UserModel = mongoose.model("User", userSchema);

module.exports = UserModel;

Above, after defining the models, we create two utility functions userSchema.pre and userSchema.methods.isValidPassword . The first function helps us hash / encrypt the user's password before adding it to the database while the second function will be called to help us validate the user's password when logging in. These models defined will be used in our application's controller which we will be defining below.

Creating our Application Controllers

Controllers are functions which hold our application's logic, here will be connecting to our database models that we defined earlier and sending data to our database through it.

Step One

To get started, create a new folder controllers in the root directory and the the following files blog.controller.js and user.controller.js . In the user.controller.js file, we are going to define our user sign-up and log-in logic. Add the following line of code to your user.controller.js file:

user.controller.js

const UserModel = require("../model/user");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
dotenv.config();

const sign_user_up = async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password || !first_name || !last_name) {
    return res.status(400).json({ error: "Email and Password Required" });
  }
  if (password.length < 6) {
    return res.status(400).json({ error: "Password too short" });
  }
  let user;
  try {
    const IsEMailRegistered = await UserModel.findOne({ email: email });
    console.log(IsEMailRegistered);
    if (IsEMailRegistered) {
      return res.status(400).json({ error: "Email Registered" });
    }
    user = await UserModel.create(req.body);
  } catch (err) {
    return res.status(500).json({ error: err });
  }
  return res
    .status(201)
    .json({ sucess: true, message: "user created sucessfully", user: user });
};

const login_user = async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password)
    return res
      .status(400)
      .json({ message: "Username and password are required." });
  if (password.length < 6) {
    return res.status(400).json({ error: "Password too short" });
  }
  try {
    const user = await UserModel.findOne({ email: email });
    if (!user) {
      return res.status(400).json({ error: "Email or Password Required" });
    }
    const validate = await user.isValidPassword(password);
    if (!validate) {
      return res.status(400).json({ error: "Email or Password Required" });
    }
    const payload = {
      email: user.email,
      first_name: user.first_name,
      last_name: user.last_name,
      id: user._id,
    };
    const token = jwt.sign(payload, process.env.JWT_SECRET, {
      expiresIn: "1h",
    });
    return res.status(200).json({ sucess: true, token: token });
  } catch (e) {
    return res.status(500).json({ error: e });
  }
};

module.exports = { sign_user_up, login_user };
  • In the sign_user_up function we are going to define the logic for user sign-up, we start by validating the inputs fields we expect from the client and returning an error if they are empty. We also check if a user has been signed up before by connecting to our database model UserModel.findOne and checking if the passed email has been sent before. If that is so, we will send an error. We finally send the data passed from the client req.body to our database through the model using the UserModel.create function and return a success message with our res parameter.

  • In the login_user function we are going to define the user log-in logic. We start by validating the inputs for emptyness. We also check for password length, we made sure it musn't be less than 6. After that, we check if user trying to login in is in our database records through the email using our model UserModel.findOne . If the user isn't found, we return an error. Next, we use the utility function user.isValidPassword defined earlier in our user.js model file to validate if the user password is correct. After that we use the user information as a payload to sign a jwt token in order to use it for user authorization. In order to sign a jwt token, we need a jwt secret which we are going to create in our .env file. Add this to your .env file: JWT_SECRET = "mysecret" .

    Note: as a best practice, your jwt secret should never be easy to guess.

    After successfully signing our jwt token, we return a success message and the token to the client.

Step two

In our blog.controller.js file, we are going to define the logic for our blogs.

Add the following lines of code:

blog.controller.js

const moment = require("moment/moment");
const BlogModel = require("../model/blog");
const { calculateReadingTime } = require("../utils");

const getPublishedBlogs = async (req, res) => {
  const {
    page = 0,
    perpage = 20,
    tag,
    author,
    title,
    created_at,
    sortby = "created_at",
    sort = "asc",
  } = req.query;

  const findQueryObj = {};
  const sortQueryObj = {};
  const sortAttributes = sortby.split(" ");
  for (const attribute of sortAttributes) {
    if (sort === "asc" && sortby) {
      sortQueryObj[attribute] = 1;
    } else if (sort === "desc" && sortby) {
      sortQueryObj[attribute] = -1;
    }
  }
  if (created_at) {
    findQueryObj.created_at = {
      $gt: moment(created_at).startOf("day").toDate(),
      $lt: moment(created_at).endOf("day").toDate(),
    };
  }
  if (author) {
    findQueryObj.author = author;
  }
  if (tag) {
    findQueryObj.tag = { $in: tag.split(" ") };
  }
  if (title) {
    findQueryObj.title = title;
  }
  findQueryObj.state = "published";
  try {
    const blogs = await BlogModel.find(findQueryObj)
      .populate("author")
      .limit(perpage)
      .skip(perpage * page)
      .sort(sortQueryObj);
    return res.status(200).json({ success: true, data: blogs });
  } catch (e) {
    return res.status(500).send("Something went wrong");
  }
};

const getAPublishedBlog = async (req, res) => {
  const { id } = req.params;
  const blog = await BlogModel.findOne({
    state: "published",
    _id: id,
  }).populate("author");

  await BlogModel.updateOne({ _id: id }, { read_count: blog.read_count + 1 });
  return res.status(200).json({ success: true, data: blog });
};

const createBlog = async (req, res) => {
  const body = req.body;
  try {
    const blog = await BlogModel.create({
      title: body.title,
      description: body.description,
      author: req.id,
      state: "draft",
      read_count: 0,
      reading_time: calculateReadingTime(body.body.length),
      tags: body.tags,
      body: body.body,
    });

    return res.status(201).json({ success: true, data: blog });
  } catch (err) {}
};

const publishBlog = async (req, res) => {
  const { id } = req.params;
  try {
    const update = await BlogModel.updateOne(
      { _id: id },
      { state: "published" }
    );
    return res.status(200).json({ success: true, data: update });
  } catch (err) {}
};

const deleteBlog = async (req, res) => {
  const { id } = req.params;
  try {
    await BlogModel.deleteOne({ _id: id });
    return res.status(200).json({ success: true, message: "Blog deleted" });
  } catch (err) {
    return res
      .status(500)
      .json({ error: err, message: "Something went wrong" });
  }
};

const getUserBlogs = async (req, res) => {
  const query = req.query;
  const perpage = query.perpage || 10;
  const page = Math.max(0, query.page || 0);
  let blogs;
  try {
    if (query.state) {
      blogs = await BlogModel.find({ state: query.state })
        .populate({
          path: "author",
          _id: req.id,
        })
        .limit(perpage)
        .skip(perpage * page);
    } else {
      blogs = await BlogModel.find({})
        .populate({
          path: "author",
          _id: req.id,
        })
        .limit(perpage)
        .skip(perpage * page);
    }
    return res.status(200).json({ success: true, data: blogs });
  } catch (err) {
    return res
    .status(500)
    .json({ error: err, message: "Something went wrong" });
  }
};
module.exports = {
  createBlog,
  deleteBlog,
  getAPublishedBlog,
  getPublishedBlogs,
  getUserBlogs,
  publishBlog,
};
  • In the getPublishedBlogs function, we return all published blog for users to read, Note that users that are not logged in will be allowed to access this. The following queries: page = 0, perpage = 20, tag, author, title, created_at, sortby = "created_at", sort = "asc" can be specified from the client to return a specific information needed from the published blogs

  • In the getAPublishedBlog function, we will return only a specific published blog. In order to know which blog to return, we will accept a query parameter from the client which is an id of the blog we want to return

  • In the createBlog function, we create a blog by passing the required fields and informations, when a blog is created with our blog model, it will be saved as a draft so it wont be returned with published blogs.

  • The publishBlog function publishes a blog provided the id passed as a query parameter. This allows the blog to the readable to every user.

  • The getUserBlogs function allows us get the blogs of a specific user/author. We use population to get the blogs of an author, only the author's id was needed. We can get both the author's published blogs and drafts.

  • The deleteBlog function deletes a blog post with a specific id passed from the client as a query parameter.

Setting Up the Application middlewares

We are going to set up two types of middleware for our applications, when a request is sent to our application through routes, we will use these middleware functions to intercept them and make the necessary validations or authorizations to decide whether to accept or decline. The two type of middleware we will be setting up are

  • Schema Validation Middleware with JOI

  • jwt authorization middleware

Schema Validation Middleware with JOI

We will need to validate the body of the request sent to the API of our blog app. I am going to demonstrate how to go about this.

To get started, create a folder in the root directory named validators and add the file blog.js in it. Proceed to add the following lines of code.

validators/blog.js

const Joi = require("joi");

const CreateBlogSchema = Joi.object({
  title: Joi.string().required(),
  description: Joi.string().required(),
  author: Joi.string().required(),
  tags: Joi.array().items(Joi.string()).required(),
  body: Joi.string().required(),
});

async function CreateBlogValidationMW(req, res, next) {
  const blogpayload = req.body;
  try {
    await CreateBlogSchema.validateAsync(blogpayload);
    next();
  } catch (error) {
    next({
      message: error.details[0].message,
      status: 400,
    });
  }
}

module.exports = {
  CreateBlogValidationMW,
};

Above we validate the fields that will be passed as a body to the the api request sent to createBlog controller. We will using this alongside our application routes later.

jwt authorization middleware

Here will will validate the jwt token sent alongside the API requests for routes that require it. To get started, create a authenticate.js in the root directory and add the following:

authenticate.js

const jwt = require("jsonwebtoken");
require("dotenv").config()

const { TokenExpiredError } = jwt;
const catchTokenExpiredError = (err, res) => {
  if (err instanceof TokenExpiredError) {
    return res
      .status(401)
      .send({ message: "Unauthorized ! Access Token was Expired" });
  }
  return res.status(401).send({ message: "Unauthorized!" });
};

const protect = (req, res, next) => {
  const authHeader = req.headers.authorization || req.headers.Authorization;
  if (!authHeader?.startsWith("Bearer "))
    return res.status(401).send({ message: "Unauthorized" });
  const token = authHeader.split(" ")[1];
  if (!token) {
    return res.status(403).send({
      message: "No token provides!",
    });
  }
  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) {
      return catchTokenExpiredError(err, res);
    }
    req.id = decoded.id;
    next();
  });
};

module.exports = protect;

We will only be accepting bearer tokens, we also handled token expiration and if a token is invalid we will reject it.

Setting up Routes for our Application

Routes are endpoints in which requests will be made to our application, let's start creating them.

To get started, create the folder routes in the root directory and add the following files blog.routes.js and user.routes.js . Add the following lines of code to them.

blog.routes.js

const express = require("express");
const protect = require("../authenticate");
const {
  getUserBlogs,
  createBlog,
  getPublishedBlogs,
  publishBlog,
  deleteBlog,
} = require("../controller/blog.controller.js");
const { CreateBlogValidationMW } = require("../validators/blog");
const blogRouter = express.Router();

blogRouter.get("/me", protect, getUserBlogs);
blogRouter.post("/create", protect, CreateBlogValidationMW, createBlog);
blogRouter.get("/", getPublishedBlogs);
blogRouter
  .patch("/:id", protect, publishBlog)
  .get("/:id", getPublishedBlogs)
  .delete("/:id", protect, deleteBlog);

module.exports = blogRouter;

Above, we import and make use of the middleware we created earlier. We protected and validated the necessary routes.

user.routes.js

const express = require("express");
const { sign_user_up, login_user } = require("../controller/user.controller.js");
const userRouter = express.Router();
userRouter.post("/signup", sign_user_up);
userRouter.post("/login", login_user);

module.exports = userRouter;

In order for our routes to work properly, we must use them in the app.js file, update the app.js file with the marked lines of code

const express = require("express");
const authRoutes = require("./routes/user.routes.js"); // new line here!
const blogRoutes = require("./routes/blog.routes.js"); // new line here!
const cors = require("cors");
require("dotenv").config();

const app = express();
app.use(cors());
app.use(express.json()); 
app.use("/auth", authRoutes); // new line here!
app.use("/blog", blogRoutes); // new line here!

app.get("/health", (req, res) => {
  res.send("App working fine ๐Ÿ‘");
});


const PORT = process.env.PORT || 3000;
connectDB();

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

Testing our Application

Now that we are done building our blog API, we have to test it, we are going to do that with postman.

Test the following endpoints and make sure your application is running on your terminal.

User Routes

sign user up

log in user

A jwt response will be returned, make sure you copy and save it

Blog Routes

In order to use most of the blog routes we need to add a bearer token to them, to do that click on the authorization tab on postman and check the bearer token type as illustrated below. Add the token gotten from the log in request below

create blogs

publish blog

get a published blog

get author's blogs

get all published blogs

delete blog

Conclusion

That's a wrap, i hope this article was free flowing and easy to understand. You can check this github repo which contains this project files.

ย