Building a Nodejs Blog API With JWT Authentication.

In this tutorial, we'll be showing you how to create a powerful blog API using Node.js, JWT, and Mongoose.

Our API will allow users to create, read, update, and delete blog posts, and we'll be using JWT (JSON Web Token) to secure the API and ensure that only authorized users can access certain routes. By the end of this tutorial.

Building an API can initially seem intimidating, especially if you're new to web development. But don't worry – we'll walk you through every step of the process, starting with setting up a new Node.js project and installing the required dependencies.

Once you have your project set up, we'll guide you through the process of creating a simple server using Express, the popular web framework for Node.js. We'll then show you how to connect to a MongoDB database using Mongoose, the popular object modeling library for MongoDB, and set up a schema for your blog posts.

When we are done with the database setup, we'll move on to defining routes for creating, reading, updating, and deleting blog posts. These routes will handle incoming HTTP requests and interact with the database to create, read, update, and delete documents as needed.

Finally, we'll show you how to implement authentication using JWT. We'll create routes for signing up and logging in, and we'll use bcrypt to hash the user's password and store it in the database. We'll also create a middleware function that verifies the JWT and attaches the decoded payload to the request object, which we can then use to secure our routes and ensure that only authenticated users can access certain routes.

By the end of this tutorial, you'll have a fully functional blog API. Let's get started building your very own blog API with Node.js, JWT, and Mongoose!

FUNCTIONALITY

  • Sign up new users

  • Login users

  • Create blog

  • Get a blog by id

  • Get all published blogs

  • Update blog

  • Delete blog

PREREQUISITES

  • Nodejs installed on your system,

  • Express.js installed on your system

  • A good knowledge of javascript and express.js

  • MongoDB to connect the app to MongoDB

SETUP

I will be using Visual Studio Code for the duration of this article, you can use any code editor of your choice. Now Let’s jump right in.

Initialize NPM:

  • Create a folder for the project, named mine Blog API.

  • Run the command on the terminal, this initializes npm and creates a package.json file in our folder.

npm init -y

Install Dependencies:

  • Type the command below to install the dependencies we will be using: bcrypt, dotenv, express, jsonwebtoken, and mongoose. We also need Nodemon a development dependency to automatically restart the server as we make changes
npm i bcrypt dotenv express jsonwebtoken mongoose
npm i nodemon -D

Create A Server:

  • Create an index.js file in the root directory of our project, inside this file, use the Require keyword to import the express Module,

  • Create a constant “app” and assign an express() function to it. This creates an Express application. The express() function is a top-level function exported by the express module.

const express = require('express');
const app = express()
  • Next, we initialize a port, create a .env file in the root folder, open it and add PORT as an environment variable and set the value to 6000.

  • Import dotenv module, this module loads environment variables from the .env file into process.env,

  • We now listen for incoming requests on the PORT, then send back a response.

  • We need to set our app to use a body parser, this is how we can receive input values. We will be using the built-in middleware function in Express

index.js file.

const express = require('express');
require('dotenv').config()

const app = express()
const PORT = process.env.PORT || 6000

app.use(express.json());
app.use(express.urlencoded({ extended: false }));


app.get("/", (req, res) => {
  res.json({ message: " Welcome to my blog" });
});
connectToDb();


app.listen(PORT, () => {
  console.log(`server listening on port ${PORT}`);
});

Connect To Database:

  • Create a file connentToDb.js, for us to connect to the MongoDB database we need a module named mongoose

  • Import the mongoose module and assign it to a constant mongoose

  • Import the dotenv module, we need our MongoDB URL. This should be saved in the .env file

connentToDb.js file.

const mongoose = require("mongoose");
require("dotenv").config();

const MONGOOSE_URL = process.env.MONGOOSE_URL;

const connectToDb = () => {
  mongoose.connect(MONGOOSE_URL);

  mongoose.connection.on("connected", () => {
    console.log("connected to MongoDb successfully");
  });

  mongoose.connection.on("error", (error) => {
    console.log("An error occurred", error);
  });
};
module.exports = connectToDb;

Database Schema:

  • Create a folder “model”

  • create a file inside the folder “userModel.js”

  • We create our user model in the “userModel.js” file

  • We add some properties to our schema and their associated SchemaType. For example, the property email will be cast to the String SchemaType

  • We then export our user model

userModel.js file

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



const userSchema = new Schema(
  {
    email: {
      type: String,
      required: true,
      unique: true,
    },

    firstName: {
      type: String,
      required: true,
    },
    lastName: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
    blog: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: "blogs",
      },
    ],
  },
  { timestamps: true }
);

const users = mongoose.model("users", userSchema);

module.exports = users;
  • Next, we create another file inside our model folder “blogModel.js”

  • Then we create our blog schema in it

  • Make sure to export the blog model also.

blogModel.js file.

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

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

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

module.exports = blogs;

BLOG FUNCTIONALITY

In this section, we will be building the actual functionality of our blog, and we will also use JWT to authenticate our users. Let’s see some definitions of so term before we proceed with the codes.

What are authentication and authorization?

Authentication and authorization are separate processes used by organizations to secure their systems and data from falling into the wrong hands. Authentication and authorization are the first lines of defense against data breaches.

Authentication is the process of identifying users and making sure users are who they say they are, authentication works through usernames, passwords, biometric information, and other information entered by the user.

Authorization is the process that determines what resource an authenticated user can access.

JWT

JSON Web Token(JWT) is an open-source standard for representing claims between two parties, The token is either signed using a private secret or a public/private key. JWT claims are usually used to pass the identity of authenticated users.

Sign Up New User:

  • Create a “authcontroller.js” file, then import the user model, bcrypt (used for password encryption), jsonwebtoken and dotenv.

  • Next, we implement the signup of new users

  • we need to get the user's input

  • Validate the user input, check if the user already exists in our database

  • Use bcrypt to encrypt the user's password

  • We then create the user in our database

  • Lastly, we create a signed JWT token and expire it after 1-hour

authcontroller.js file

const users = require("../models/usersModel");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
require("dotenv").config();

// Define a function to create a token

const signToken = (id) => {
  return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1h" });
};

//SIGN UP NEW USERS
const signup = async (req, res) => {
  try {
    const { email, firstName, lastName, password } = req.body;

    if (!(email && firstName && lastName && password)) {
      return res.status(400).send("All Input Is Required");
    }
    const olderUser = await users.findOne({ email });
    if (olderUser) {
      return res.status(409).send("User Exist, Please Login. ");
    }
    const hashpassword = await bcrypt.hash(password, 12);
    const newUser = new users({
      firstName,
      lastName,
      email: email.toLowerCase(),
      password: hashpassword,
    });

    const token = signToken(newUser._id);
    const user = await newUser.save();

    res.status(200).json({ status: "success", token, user });
  } catch (error) {
    res.status(500).json(error.message);
  }
};

Login User:

  • We get user input and validate the input

  • Check our database to ensure the user exists

  • Compare the user password against the password we saved earlier in our database

  • Lastly, we create a signed token for our user

authcontroller.js file continues here

const login = async (req, res) => {
  try {
      const {email, password} = req.body

    if(!(email && password)){
        res.status(400).json("All input is required ")
    }



    const user = await users.findOne({ email: req.body.email });

    if (!user) {
      return res.status(400).json("Wrong Details, Try Again");
    }

    const match = await bcrypt.compare(req.body.password, user.password);

    if (!match) {
      return res.status(400).json("Wrong password, Try Again");
    }

    const token = signToken(user._id);

    res.status(200).json({ user, token });
  } catch (error) {
    res.status(500).json(error.message);
  }
};
module.exports = { signup, login };

Next, we create a file “blogController.js”. This file will contain all the functions that we need to do the basic CRUD operations on our Blog API.

Create Blogs:

  • Get user input from the request body

  • Get the user from our database using the ID from the request body

  • Create the blog using the input from the user, and save it to our database

  • Add the blog to an array of blogs created by the user

  • Send a successful message or send the error if we encounter one

A function to create a new blog.

const createBlog = async (req, res) => {
  try {
    const { title, description, body, tags } = req.body;
    const { id } = req.user;
    const user = await users.findById(id);
    const author = ${user.firstName} ${user.lastName};
    const newBlog = new blogs({
      title,
      description,
      body,
      author,
      tags,
      user: user._id,
    });
    const blogPost = await newBlog.save();
    user.blog = user.blog.concat(blogPost._id);
    await user.save();
    res.status(200).json({ blogPost });
  } catch (error) {
    res.status(500).json(error.message);
  }
};

Get Blog By Id:

  • Access the id of the blog from the request params

  • Use the find by id method and pass the id as an argument

  • This returns the requested blog and we send it back as a response or error if we encounter one

Function to get a blog by id

const getABlog = async (req, res) => {
  try {
    const id = req.params.id;
    const blog = await blogs.findById(id).where({ state: "published" });
    if (!blog) {
      res.status(404).json("NO blog found!");
    }
    blog.readCount++;
    await blog.save();
    return res.status(200).json(blog);
  } catch (error) {
    return res.status(500).json(error.message);
  }
};

Get All Published Blogs:

  • Create a copy of the request query and assign it to an object queryObj

  • Create an array excludedFields, this list contains query parameters to be excluded from the query

  • Iterate through the list and remove it from the queryObj

  • The query variable is then assigned the result of calling the find method on the Blogs and passing in the queryObj as an argument. This returns a list of all the blog posts matching the specified query parameters

  • If the sort query parameter is present in the request, we sort using the specified field else we sort using the createdAt field in descending order

  • Define the page variable and assign the value of the page query parameter to it, if the page query parameter is not present, we set page to 1. Repeat the same process for the limit query parameter; if it's not present, we set the limit to 20.

  • Define the skip variable and set it to the product of (page - 1) and limit. This calculates the number of documents to skip based on the specified page and limit values.

  • The page, limit, and skip variables help in pagination and limiting the number of documents returned

  • Finally, we retrieve all published blog posts by calling the find() method on Blogs and passing the query object as an argument. If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.

A function to get a list of all published blogs

const getAllBlogs = async (req, res) => {
  try {
    const queryObj = { ...req.query };



    //FILTERING
    const excludedFields = ["page", "sort", "limit", "fields"];
    excludedFields.forEach((el) => delete queryObj[el]);
    let query = blogs.find(queryObj);

    //SORTING
    if (req.query.sort) {
      const sortBy = req.query.sort.split(",").join(" ");
      query = query.sort(sortBy);

    } else {
      query = query.sort("-createdAt");

    }

    //PAGINATION
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const skip = (page - 1) * limit;

    if (req.query.page) {
      const numOfArticle = await blogs
        .countDocuments()
        .where({ state: "published" });
      if (skip >= numOfArticle) throw new Error("page does not exist");
    }
    query = query.skip(skip).limit(limit);


    const publishedBlogs = await blogs
      .find(query)
      .where({ state: "published" })
      .populate("user", { firstName: 1, lastName: 1, _id: 1 });

    res.status(200).json(publishedBlogs);
  } catch (error) {
    res.status(500).json(error.message);
  }
}

Update Blog By Id:

  • Define an id variable and set it to equal req.params.id

  • Define a user variable and assign the user property of the req object to it

  • Next, destructure the req.body and assign its properties to the variables with the same name

  • Retrieve the blog post with the specified id by calling the findById method on the Blogs object and passing the id as an argument

  • Next, we compare the id property of the blog.user object to the id property of the user object. This checks that the user making the request is the owner of the blog post. If the user is the owner, we update the blog post by calling the findByIdAndUpdate() method on the Blogs object passing in the id variable, an object containing the updated data, and an option object set to true.

  • We save the updated blog post and send a response back with a status code of 200 and the updated blog post. If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.

A function to update a blog by id

const updateBlog = async (req, res) => {
  try {
    const id = req.params.id;
    const user = req.user;
    const { title, description, state, tags, body } = req.body;


    const blog = await blogs.findById(id);

    if (blog.user._id.toString() === user.id) {
      try {
        const updatedBlog = await blogs.findByIdAndUpdate(
          id,
          { title, description, state, tags, body, readingTime },
          { new: true }
        );

        await updatedBlog.save();
        res.status(200).json(updatedBlog);
      } catch (error) {
        res.status(500).json(error.message);
      }
    } else {
      res.status(401).json("Unauthorized");
    }
  } catch (error) {
    res.status(500).json(error.message);
  }
};

Getting Personal Blogs:

  • Define the variable userid and set it to the id property of the user object in the req object

  • Define the page variable and set it to the value of the page query parameter. If the page query parameter is not present, the page variable is set to 1

  • Define the limit variable and set it to the value of the limit query parameter. If the limit query parameter is not present, the limit variable is set to 20

  • Define the skip variable and set it to the product of (page - 1) and limit. This calculates the number of pages to skip based on the specified page and limit value

  • Retrieve all blog posts by the specified user by calling the find() method on the Blogs object passing an object with the user field set to the userid variable as an argument

  • Send a response back with a status code of 200 and the list of the user’s blog posts. If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.

A function to get back personal blogs

const getUserBlogs = async (req, res) => {
  try {
    const userid = req.user.id;
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const skip = (page - 1) * limit;
    const userBlogs = await blogs
      .find({ user: userid })
      .skip(skip)
      .limit(limit);
    return res.status(200).json(userBlogs);
  } catch (error) {
    return res.status(500).json(error.message);
  }
};

Delete a Blog By Id:

  • Define an id variable and set it to equal req.params.id

  • Define a user variable and assign the user property of the req object to it

  • Retrieve the blog post with the specified id by calling the findById method on the Blogs object and passing the id as an argument

  • Next, we compare the id property of the blog.user object to the id property of the user object. This checks that the user making the request is the owner of the blog post

  • Delete the blog post by calling the deleteone() method on the Blogs object

  • Send a response with a status code of 200 and a message indicating that the blog was deleted successfully

  • If the user is not the owner of the blog post, send a JSON response with a status code of 401 and the string “unauthorized”

  • If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.

A function to delete blog by id

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

    const blog = await blogs.findById(id);

    if (blog.user._id.toString() === user.id) {
      try {
        await blogs.deleteOne();
        res
          .status(200)
          .json({ status: "success", message: "blog deleted successfully" });
      } catch (error) {
        res.status(500).json(error);
      }
    } else {
      res.status(401).json("Unauthorized");
    }
  } catch (error) {
    res.status(500).json(error);
  }
};

Next, we export all the functions

module.exports = {
  createBlog,
  getABlog,
  getAllBlogs,
  updateBlog,
  getUserBlogs,
  deleteBlog,
};

Authentication Middleware:

We need a middleware function to protect routes that require authentication. The function checks the authorization header of the request to see if it contains a valid JWT token, retrieves the user associated with the token, and then assigns the user to the req object. This allows the subsequent route handler function to have access to the authenticated user. If the authorization header is not present or the token is invalid, the function sends a response to the client indicating that the user is unauthorized.

authmiddleware.js

const jwt = require("jsonwebtoken");
const users = require("../models/usersModel");

//PROTECTING ROUTES

const isAuthenticated = async (req, res, next) => {
  let token;
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith("Bearer")
  ) {
    token = req.headers.authorization.split(" ")[1];
  }
  if (!token) {
    return res.status(401).json("Unauthorized!. Please login");
  }

  const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
  const user = await users.findById(decodedToken.id);
  if (!user) {
    return res.status(401).json("authorization not found");
  }
  req.user = user;
  next();
};

module.exports = isAuthenticated;

Routing:

Next, we handle our routing

blogroute.js

const blogRouter = require("express").Router();
const isAuthenticated = require("../middleware/authmiddleware.js");
const blogcontroller = require("../controller/blogcontroller");

blogRouter
  .route("/")
  .get(blogcontroller.getAllBlogs)
  .post(isAuthenticated, blogcontroller.createBlog);

blogRouter.route("/:id").get(blogcontroller.getABlog);

blogRouter
  .route("/owner/:id")
  .get(isAuthenticated, blogcontroller.getUserBlogs)
  .put(isAuthenticated, blogcontroller.updateBlog)
  .delete(isAuthenticated, blogcontroller.deleteBlog);

module.exports = blogRouter;

authroute.js

const authcontroller = require("../controller/authcontroller");
const userRouter = require("express").Router();

userRouter.route("/signup").post(authcontroller.signup);
userRouter.route("/login").post(authcontroller.login);

module.exports = userRouter;