How to build a blog API with nodejs

How to build a blog API with nodejs

Introduction

In this article, we are going to create a blog API using Node.js. A Blog API in which users can signup, create, read, update and delete blogs, and even filter blogs with various parameters.

Technology Stack

  • Back-end: Node.js, Express

  • Database: MongoDB

Prerequisites

  • Basic knowledge of JavaScript, and MongoDB

  • Node.js and npm installed on your machine

  • MongoDB server installed and running on your machine

Implementation

Below is the step-by-step implementation of the above approach.

Create a Project Folder

It is good practice to have all files and folders properly organized on the system. For this blog project, we will create a dedicated folder on the computer. We can do this with the computer's file manager, VS Code or the terminal. Here's the terminal script to create a new directory (folder) and then move into it;

$ mkdir blogApp

$ cd blogApp

Initialize NPM

Locate your project folder into the terminal & type the command

npm init -y

It initializes our node application & makes a package.json file.

Install Dependencies

Locate your root project directory and open terminal, these packages are required to get the project started, we’d install more packages as needed.

npm install express nodemon mongoose morgan

Structure of the project

We are gonna adopt an MVC structure for this project, MVC stands for model view controllers. The model folder would contain all the database schema and database logic, controllers would contain all the request and response handlers, and it could also contain functions that are meant to manipulate data

Install dotenv package and add a .env file. This file would contain all our environment variables and secret keys etc

npm i dotenv

.env

PORT = 3030

Let's test that our server is working by creating our app.js file. This would serve as our entry point

app.js

const express = require('express');
const PORT = process.env.PORT || 3000;
const app = express();

app.use(express.json());
app.get('/', (req, res) => {
res.end('Home Page');
});

app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})

to start up server run

nodemon app.js

Setup database

Add our mongodb uri to the .env file check here for details on how to get connection uri

Create a db folder and add a connection.js file

connection.js

const moogoose = require('mongoose');
require('dotenv').config();

const MONGODB_URI = process.env.MONGODB_URI;

// connect to mongodb
function connectDB() {
    moogoose.connect(MONGODB_URI);

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

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

module.exports = { connectDB };

Update the app.js to connect

const express = require('express');
const PORT = process.env.PORT || 3000;
const connectDB = require("./db/connection");


const app = express();
connectDB()
app.get('/', (req, res) => {
    res.end('Home Page');
});

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`)
})

Models

We have set up our db now let us set up our models for the app. We need to set up a user model and a blog model. Add user.js and blog.js to models folder

user.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const bcrypt = require("bcrypt");
const userSchema = new Schema({
  firstName: {
    type: String,
    required: true,
  },
  lastName: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
  },
  passWord: {
    type: String,
    required: true,
  },
  blogs: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Blog",
    },
  ],
});

userSchema.set("toJSON", {
  transform: (document, returnedObject) => { 
    delete returnedObject.__v;
    delete returnedObject.passWord;
  },
});

// Encrypt password before saving to the database
userSchema.pre("save", async function (next) {
  const user = this;
  if (!user.isModified("passWord")) return next();
  const hash = await bcrypt.hash(
    this.passWord,
    10
  );
  this.passWord = hash;
  next();
});

userSchema.methods.comparePasswords =
  async function (password) {
    return await bcrypt.compare(
      password,
      this.passWord
    );
  };
const User = mongoose.model("User", userSchema);
module.exports = User;

As observed we added a 3rd party library to hash our passwords before saving to db. Type the following code in the terminal

npm i bcrypt

blog.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const readingTime = require("../utils/readingAlgo");

const blogSchema = new Schema(
  {
    title: {
      type: String,
      required: true,
      unique: true,
    },
    description: String,
    author: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },
    state: {
      type: String,
      default: "draft",
      enum: ["draft", "published"],
    },
    readCount: {
      type: Number,
      default: 0,
    },
    readingTime: Number,
    tags: [String],
    body: {
      required: true,
      type: String,
    },
  },
  { timestamps: true }
);

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

module.exports = Blog;

Validation

Next step we need to add validations to check user input and handle wrong user input with appropriate error messages. We would be using joi for validation

npm i joi

Create a validations folder and add validation.js

validation.js

const joi = require("joi");

const validateUserSignup = async (
  req,
  res,
  next
) => {
  const userPayload = req.body;

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

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

const blogValidator = joi.object({
  title: joi.string().min(5).max(255).required(),
  description: joi
    .string()
    .min(5)
    .max(255)
    .optional(),
  // author: joi.objectId().required(),
  body: joi.string().required(),
  state: joi.string().default("draft"),
  readCount: joi.number().default(0),
  tags: joi.string().optional(),
});
const userSignupValidator = joi.object({
  firstName: joi.string().required(),
  lastName: joi.string().required(),
  email: joi.string().required(),
  passWord: joi
    .string()
    .min(5)
    .max(255)
    .required(),
});
const userLoginValidator = joi.object({
  email: joi.string().required(),
  passWord: joi
    .string()
    .min(5)
    .max(255)
    .required(),
});

module.exports = {
  validateBlogMiddleWare,
  validateUserSignup,
  validateUserLogin,
};

Controllers

Next , we need to set up controllers to handle requests and give out responses. For our users, we need to set up a strategy that handles login and sign up

We would use passport and JWT for authentication

npm i passport-jwt passport-local passport jsonwebtoken

Create a file passport.js

passport.js

const passport = require("passport");
const localStrategy =
  require("passport-local").Strategy;
const User = require("../model/users.model");
const JWTstrategy =
  require("passport-jwt").Strategy;
const ExtractJWT =
  require("passport-jwt").ExtractJwt;
require("dotenv").config();

passport.use(
  "signup",
  new localStrategy(
    {
      usernameField: "email",
      passwordField: "passWord",
      //ALLOWS REW PARAMS TO BE PASSED
      passReqToCallback: true,
    },
    async (req, email, passWord, done) => {
      try {
        const { firstName, lastName } = req.body;

        const user = await User.create({
          firstName,
          lastName,
          email,
          passWord,
        });

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

passport.use(
  "login",
  new localStrategy(
    {
      usernameField: "email",
      passwordField: "passWord",
    },
    async (email, passWord, done) => {
      try {
        const user = await User.findOne({
          email,
        });

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

        const validate =
          await user.comparePasswords(passWord);

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

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

passport.use(
  new JWTstrategy(
    {
      secretOrKey: process.env.JWT_SECRET,
      jwtFromRequest:
        ExtractJWT.fromAuthHeaderAsBearerToken(),
    },
    async (token, done) => {
      try {
        const user = await User.findById(
          token.user
        );
        if (user) {
          return done(null, user);
        }
        return done(null, false);
      } catch (e) {
        next(err);
      }
    }
  )
);

To make use of jwt we need to add a secret key to the .env file

.env

JWT_SECRET = {add your secret key}

Add a user.controller.js file to the controller folder

user.js

const User = require("../model/users.model");
const passport = require("passport");
const jwt = require("jsonwebtoken");

const signup = async (req, res, next) => {
  res.json({
    message: "Signup successful",
    user: req.user,
  });
};
const login = async (req, res, next) => {
  passport.authenticate(
    "login",
    async (err, user, info) => {
      try {
        if (err || !user) {
          const error = new Error(
            "Wrong email/password"
          );
          return next(error);
        }

        req.login(
          user,
          { session: false },
          async (error) => {
            if (error) return next(error);

            const body = {
              _id: user._id,
              email: user.email,
            };
            const token = jwt.sign(
              { user: body },
              process.env.JWT_SECRET,
              { expiresIn: "1h" }
            );

            return res.json({
              message: "login successful",
              token,
            });
          }
        );
      } catch (error) {
        console.log(error);
        next(error);
      }
    }
  )(req, res, next);
};
module.exports = {
  signup,
  login,
};

User routes

We need to mount our routes and setup endpoints Add a user.routes.js

user.routes.js

const userController = require("../controllers/users.controller");
const passport = require("passport");

const {
  signup,
  login,
} = require("../controllers/users.controller");
require("../authentication/passport");
const {
  validateUserSignup,
  validateUserLogin,
} = require("../validations/validator");

const userRouter = require("express").Router();

userRouter.get("/users", userController.getUsers);

userRouter.post(
  "/signup",
  validateUserSignup,
  passport.authenticate("signup", {
    session: false,
  }),
  signup
);
userRouter.post(
  "/login",
  validateUserLogin,
  login
);

module.exports = userRouter;

Update app.js

app.js

const express = require('express');
const PORT = process.env.PORT || 3000;
const userRouter = require("./routes/user.routes");

const app = express();
app.use(express.json());
app.use("/", userRouter);

app.get('/', (req, res) => {
    res.end('Home Page');
});

app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})

After updating our app.js file, let’s now start test our user endpoints using postman or thunder client.

Blog Controllers

Next we create our blog controllers to handle different requests as follows

[if !supportLists]1. [endif]Create a new post

[if !supportLists]2. [endif]Get all posts

[if !supportLists]3. [endif]Get single post

[if !supportLists]4. [endif]Get all posts belonging to a logged in user

[if !supportLists]5. [endif]Get single post belonging to a logged in user

[if !supportLists]6. [endif]Update the post body only if it’s the owner of the post making the request

[if !supportLists]7. [endif]Update state of a post only if it’s the owner of the post making the request

[if !supportLists]8. [endif]Delete a post only if it’s the owner of the post making the request

Create a blogs.controller.js in controller folder

blog.controller.js

const Blogs = require("../model/blog.model");
const Users = require("../model/users.model");
const mongoose = require("mongoose");

const getAllBlogs = async (req, res) => {
  const queryParam = {
    state: "published",
  };
  const {
    page = 1,
    limit = 20,
    sortBy,
    author,
    title,
    tags,
  } = req.query;
  const { orderBy } = req.query || "asc";
  const p = page || 1;
  let blogsPerPage = (p - 1) * limit;

  const sort = {};
  if (sortBy && orderBy) {
    sort[sortBy] = orderBy === "asc" ? 1 : -1;
  }
  if (author) queryParam.author = author;
  if (title) queryParam.title = title;
  try {
    const blogs = await Blogs.find(queryParam)
      .select("title description  author -_id")
      .limit(limit)
      .skip(blogsPerPage)
      .sort(sort);

    if (blogs.length === 0) {
      res.status(404);
      res.send("Blog Not Found");
      return;
    }

    res.json({ status: true, blogs });
  } catch (err) {
    console.log(err);
    return res.status(400);
  }
};

//LOGGED IN AND NON LOGGED IN USERS GET PUBLISHED BLOG
const getBlog = async (req, res) => {
  const { query } = req;
  const { title, id } = query;
  const queryParam = {};

  if (title) queryParam.title = title;
  if (id) queryParam._id = id;
  try {
    const blog = await Blogs.findOne(
      queryParam
    ).populate({
      path: "author",
      select: "firstName lastName -_id",
    });

    if (!blog) {
      res.status(404).send({
        status: false,
        message: "Blog not found",
      });
      return;
    }
    if (blog.state !== "published") {
      res.status(404);
      res.json({
        status: false,
        message: "Blog not found",
      });
      return;
    }
    blog.readCount = blog.readCount + 1;
    await blog.save();

    res.status(200);
    res.json({ status: true, blog });
    return;
  } catch (err) {
    res.status(400);
    res.json({
      status: false,
    });
    return;
  }
};

const getBlogById = async (req, res, next) => {
  const { id } = req.params;
  try {
    const blog = await Blogs.findOneAndUpdate(
      { _id: id, state: "published" },
      { $inc: { readCount: 1 } },
      { new: true }
    ).populate({
      path: "author",
      select: "-email -blogs",
    });
    if (!blog) {
      res.status(404);
      res.json({
        status: false,
        message: "Blog not found",
      });
      return;
    }
    res.status(200);
    res.json(blog);
    return;
  } catch (err) {
    next(err);
  }
};
const newBlog = async (req, res) => {
  const body = req.body;
  const author = req.user.id;
  body.author = author;

  const wordCount = body.body.split(" ").length;
  body.readingTime = ((wordCount) => {
    return Math.round((wordCount / 200) * 60);
  })(wordCount);

  try {
    const user = await Users.findById(author);
    const blog = await Blogs.create(body);

    user.blogs = user.blogs.concat(blog._id);
    await user.save();
    res.status(201);
    res.json({ status: true, blog });
  } catch (err) {
    res.status(400);
    res.send("Bad request");
  }
};

const editBlog = async (req, res) => {
  const { id } = req.params;
  const { title, description, body, tags } =
    req.body;

  await Blogs.findByIdAndUpdate(
    { _id: id },
    {
      title,
      description,
      body,
      $push: { tags: tags },
    },
    { new: true }
  );
  return res.status(201).json({
    message: "Blog successfully updated",
  });
};

const updateBlog = async (req, res) => {
  const { id } = req.params;
  try {
    const blog = await Blogs.findOneAndUpdate(
      {
        _id: id,
        state: "draft",
      },
      { $set: { state: "published" } },
      { new: true }
    ).select("state title description -_id");
    if (!blog) {
      res.status(400);
      res.send("Blog published already");
      return;
    }

    res.status(200).json({
      message: "Blog state successfully updated",
      blog,
    });
  } catch (err) {
    next();
  }
};

const deleteBlog = async (req, res) => {
  const { id } = req.params;

  try {
    Blogs.deleteOne({ _id: id }, function (err) {
      if (err) return res.send(err);
      res.status(200);
      res.json({
        status: true,
        message: "Deleted",
      });
    });
    return;
  } catch (error) {
    next();
  }
};
const getMyBlogs = async (req, res) => {
  const author = req.user.id;
  const _id = mongoose.Types.ObjectId(author);

  const {
    state,
    page = 1,
    limit = 20,
    sortBy,
    orderBy,
  } = req.query;

    const p = page;
  let blogsPerPage = (p - 1) * limit;
  const sort = {};
  if (sortBy && orderBy) {
    sort[sortBy] = orderBy === "asc" ? 1 : -1;
  }
  let blog;
  try {
    if (state) {
      blog = await Blogs.find({
        author: _id,
        state: state,
      })
        .select("title description state")
        .limit(limit)
        .skip(blogsPerPage)
        .sort(sort);
    } else {
      blog = await Blogs.find({
        author: _id,
      })
        .select("title description state")
        .limit(limit)
        .skip(blogsPerPage)
        .sort(sort);
    }
    if (blog.length != 0) {
      res.status(200);
      res.json({
        status: true,
        blog: blog,
      });
    } else {
      res.status(404).send("No Blogs created");
      return;
    }
  } catch (error) {
    next(error);
  }
};
const getMyBlogById = async (req, res) => {
  const { id } = req.params;
  try {
    const blog = await Blogs.findOneAndUpdate(
      { _id: id },
      { $inc: { readCount: 1 } },
      { new: true }
    );
    if (!blog) {
      res.status(404);
      res.json({ status: false });
      return;
    }

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

module.exports = {
  updateBlog,
  deleteBlog,
  getMyBlogs,
  getMyBlogById,
  getBlog,
  getAllBlogs,
  editBlog,
  newBlog,
  getBlogById,
};

We need to add an algorithm that calculates reading time and adds it to database before saving hence we add the following code to our blog model

// calculate reading time before saving document
blogSchema.pre("save", function (next) {
  let blog = this;
  // do nothing if the article body is unchanged
  if (!blog.isModified("body")) return next();
  // calculate the time in minutes
  const readTime = readingTime(this.body);
  blog.readingTime = readTime;
  next();
});

Blog Routes

Next we need to create our endpoints that receives the requests

Add a file called blog.routes.js in the routes folder

blog.routes.js

const blogRouter = require("express").Router();
require("../authentication/passport");
const passport = require("passport");
const authorize = require("../authentication/authorize");
const blogController = require("../controllers/blogs.controllers");
const {
  validateBlogMiddleWare,
} = require("../validations/validator");

blogRouter
  .route("/blogs")
  .get(blogController.getAllBlogs);
blogRouter
  .route("/blog")
  .get(blogController.getBlog);
blogRouter
  .route("/getblog/:id")
  .get(blogController.getBlogById);

blogRouter.use(
  "/",
  passport.authenticate("jwt", {
    session: false,
  })
);
blogRouter
  .route("/blogs")
  .post(
    validateBlogMiddleWare,
    blogController.newBlog
  );


blogRouter
  .route("/myblogs")
  .get(blogController.getMyBlogs);

blogRouter
  .route("/myblogs/:id")
  .get(blogController.getMyBlogById)
  .put(blogController.editBlog)
  .patch(blogController.updateBlog)
  .delete(blogController.deleteBlog);

module.exports = blogRouter;

We need to mount to be able to test our blog app

Update app.js

app.js

const blogRouter = require("./routes/blog.routes");


app.use("/api", blogRouter);

After updating our app.js file, let’s now test our blog endpoints.

Authorization

Next we add authorization to allow only authorize users access certain endpoints and make certain requests

Create a folder named authentication and add file authorize.js

authorize.js

const Blogs = require("../model/blog.model");

const authorize = async (req, res, next) => {
  const author = req.user.id;
  const { id } = req.params;
  try {
    const blog = await Blogs.findOne({ _id: id });

    if (!blog) {
      res.status(404).send({
        message: "Blog not found",
        status: false,
      });
      return;
    }
    const blogId = blog.author.valueOf();

    if (blogId === author) {
      next();
    } else {
      throw new Error("auth");
    }
  } catch (err) {
    next(err);
  }
};

module.exports = authorize;

This file serves as a middleware to check if authenticated and authorized users are trying to access a protected route

To add update the blog.routes.js file

const blogRouter = require("express").Router();
require("../authentication/passport");
const passport = require("passport");
const authorize = require("../authentication/authorize");
const blogController = require("../controllers/blogs.controllers");
const {
  validateBlogMiddleWare,
} = require("../validations/validator");

blogRouter
  .route("/blogs")
  .get(blogController.getAllBlogs);
blogRouter
  .route("/blog")
  .get(blogController.getBlog);
blogRouter
  .route("/getblog/:id")
  .get(blogController.getBlogById);

blogRouter.use(
  "/",
  passport.authenticate("jwt", {
    session: false,
  })
);
blogRouter
  .route("/blogs")
  .post(
    validateBlogMiddleWare,
    blogController.newBlog
  );

blogRouter
  .route("/myblogs")
  .get(blogController.getMyBlogs);

blogRouter.use("/myblogs/:id", authorize);
blogRouter
  .route("/myblogs/:id")
  .get(blogController.getMyBlogById)
  .put(blogController.editBlog)
  .patch(blogController.updateBlog)
  .delete(blogController.deleteBlog);

module.exports = blogRouter;

Error Handling

Express handles errors that occur synchronously we need to add custom error handlers to handle asynchronous errors

Create a folder middlewares and add errorHandler.js

errorHandler.js

const errorHandler = (err, req, res, next) => {
  if (err.message === "auth") {
    res.status(401);
    res.send("Unauthorized");
    return;
  } else if (err.type === "input") {
    res.status(400);
    return res.send("invalid input");
  } else if (
    err.message.startsWith(
      "Cast to ObjectId failed"
    )
  ) {
    res.status(400);
    return res.send("Invalid Blog ID");
  } else if (
    err.message.startsWith(
      "E11000 duplicate key error collection"
    )
  ) {
    res.status(400).json({
      message: "email already in use",
    });
  } else if (
    err.message.startsWith(
      "User validation failed"
    )
  ) {
    res.status(400).json({
      message: "Invalid/Incomplete details",
    });
  } else {
    res.status(500);
    return res.send("Internal error");
  }
};

const invalidPathHandler = (req, res, next) => {
  res.status(400).send({
    message: `${req.originalUrl} is not a valid path`,
  });
  next();
};

module.exports = {
  errorHandler,
  invalidPathHandler,
};

Now we add the error handler middleware after all others so that all errors are passed down with the next() function until it gets to the handler

Update app.js with the following code

const express = require('express');
const PORT = process.env.PORT || 3000;
const userRouter = require("./routes/user.routes");
const blogRouter = require("./routes/blog.routes");
const {
  errorHandler,
  invalidPathHandler,
} = require("./middlewares/errorHandlers");


const app = express();
app.use(express.json());
app.use("/", userRouter);
app.use("/api", blogRouter);
app.get('/', (req, res) => {
    res.end('Home Page');
});
app.all("*", invalidPathHandler);

app.use(errorHandler);

app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})

API Security

We can add some third party libraries to add security to our api

npm i express-rate-limit helmet

Rate limiting is a technique to limit network traffic to prevent users from exhausting system resources. Rate limiting makes it harder for malicious actors to overburden the system and cause attacks like Denial of Service (DoS) and Helmet helps you secure your Express apps by setting various HTTP headers.

Update the app.js

const express = require('express');
const PORT = process.env.PORT || 3000;
const userRouter = require("./routes/user.routes");
const blogRouter = require("./routes/blog.routes");
const {
errorHandler,
invalidPathHandler,
} = require("./middlewares/errorHandlers");
const rateLimit = require("express-rate-limit");
const helmet = require("helmet");

const limiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the RateLimit-* headers
legacyHeaders: false, // Disable the X-RateLimit-* headers
});

//add secuirty
app.use(helmet());

app.use(express.json());
app.use(morgan("dev"));

// Apply the rate limiting middleware to API calls only
app.use("/api", limiter);
app.use("/", userRouter);
app.use("/api", blogRouter);

app.all("*", invalidPathHandler);
app.use(errorHandler);

app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})

Deployment

Push codes to github and deploy to a hosting service such as Heroku, cyclic or render. Set up the appropriate environment variables for the hosting services. Always make sure to add .env file that contains sensitive information to .gitignore to prevent it from being uploaded to github

Wrap Up

We have come to the end of this tutorial, and we have been able to build a Node.js Blog API, where Users can Sign up, Create and publish their blog posts/articles, update, edit and delete their articles also we touched on input validations, authentication, authorization, error handling and API security.

I hope you learnt a lot from this tutorial. All codes used can be found at GitHub