Creating a Blog API with nodejs using express and mongodb: JWT Authentication

lysakb

Borokinni Elizabeth

Posted on January 8, 2023

Creating a Blog API with nodejs using express and mongodb: JWT Authentication

In this article, we will discuss how to use NodeJS to develop a BLOG API. This API consist of users and the blogs they create whereby the user sign up and sign in using JWT Strategy. And only authenticated users can create or edit a blog. Other features can also be added to the blog such as; rate limiting, reading time, pagination and security measures.

The Blog API has a CRUD functionality with the use of express and mongodb. CRUD which is an acroynm of Create, Read, Update and Delete are fundamental or basic building blocks of a backend system. It is basically a set of operation requests that is carried out by servers which helps in constructing the API (POST, GET, PUT and DELETE requests respectively).

In order to create this API, we need nodejs which is a JavaScript runtime and NPM, a node package manager that usually comes with nodejs will be used to install other packages like Express that are required to create this API.

Express is a nodejs web application framework that offers a wide variety of features for creating web and mobile applications. It is a layer on top of nodejs. Lastly we need a database for storing users information, in this case, mongodb. MongoDb is a NoSQL database that saves information as a document-like JSON object.

JWT Authentication is a way to authenticate users which means to verify the identity of a user. JWT stands for jsonwebtoken. It enables stateless user authentication without actually storing any data about the users on the system. It uses a token-based authentication whereby users obtain token that allows them to access a particular resource.

GETTING STARTED

A folder should be created for the project, then you run npm init in the terminal. This creates a package.json file which contains information about the project dependencies and packages.

$ npm init
Enter fullscreen mode Exit fullscreen mode

This begins the initialization process whereby certain information such as the project name, description will have to be provided regarding the project.

Packages To Install

  • nodemon
  • express
  • dotenv
  • mongoose
  • bcrypt
  • jsonwebtoken
  • mongoose-paginate-v2
  • supertest
  • helmet
  • jest
  • express-rate-limit
$ npm install nodemon express dotenv mongoose mongoose-paginate-v2 bcrypt jsonwebtoken express-rate-limit helmet jest supertest
Enter fullscreen mode Exit fullscreen mode

The nodemon package is installed as a dependency so that the server restarts automatically.
dotenv package automatically imports environment variables into the process.env object from the .env file.
Bcrypt is a module that stores passwords as hashed passwords instead of plaintext. This helps to secure passwords inputted by the user.
mongoose-paginate-v2 is a package used for pagination.
supertest and jest is used for test.
helmet is a nodejs package that protects the server by setting appropriate HTTP response.
express-rate-limit is a limiting middleware that limits the amount of requests sent to the API.

Package.json file

{
  "dependencies": {
   "bcrypt": "^5.1.0",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-rate-limit": "^6.7.0",
    "helmet": "^6.0.1",
    "jest": "^29.2.2",
    "jsonwebtoken": "^9.0.0",
    "mongoose": "^6.7.0",
    "mongoose-paginate-v2": "^1.7.1",
    "nodemon": "^2.0.20",
    "supertest": "^6.3.1"
  },
  "name": "blog_app",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

Enter fullscreen mode Exit fullscreen mode

Create .env file which would contain all your environment variables with their respective values. This usually consist of sensitive data such as keys and password and should not be pushed to github. Therefore a .gitignore file can be created to prevent accidental pushing to the git repo.

$ .env
PORT = value
MONGO_DB_CONNECTION_URL = mongodb+srv://username:password@blog.whb5sge.mongodb.net/?retryWrites=true&w=majority
SECRET_TOKEN = value
Enter fullscreen mode Exit fullscreen mode
$gitignore
// add the .env file 
node_modules
.env
Enter fullscreen mode Exit fullscreen mode

Create app.js file

const express = require('express');

// import the .env file
require('dotenv').config();

const app = express()

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

app.get("/", (req, res)=>{
    res.send("i am working")
})

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

Create database folder which contains database.js file to enable connection to the mongodb database.

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

const MONGO_DB_CONNECTION_URL = process.env.MONGO_DB_CONNECTION_URL

function connectmongodb(){
    mongoose.connect(MONGO_DB_CONNECTION_URL);

   mongoose.connection.on('connected', ()=>{
        console.log('connected to mongodb successfully')
    })
    mongoose.connection.on("error", (err)=>{
        console.log(err)
        console.log("error occurred")
    })
}
module.exports = {connectmongodb}

Enter fullscreen mode Exit fullscreen mode

Create index.js file which will be the entry point of our application
index.js

const app = require("./app");
const {connectmongodb} = require('./database/database');

const PORT = process.env.PORT;
connectmongodb()

app.listen(PORT, ()=>{
    logger.info(`listening at ${PORT}`)
})
Enter fullscreen mode Exit fullscreen mode

MODELS
Now we create a model folder which we would use to define our Schema for both the user and the blog. A schema defines the structure of a particular document.
Create a userModel.js file inside the model folder.
This is where we would define the user Schema.

const express = require('express');
const mongoose = require('mongoose');
// bcrypt for hashing of passwords
const bcrypt = require("bcrypt");

const Schema = mongoose.Schema;

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

    last_name:{
        type: String,
        required: true
    },

    email:{
        type: String,
        required: true,
        unique: true
    },

    password:{
        type: String,
        required: true
    },

    article: [
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'blog',
        },
    ],

})

//hashing the password
userSchema.pre("save", 
    async function(next){
        const user = this;
        if(!user.isModified('password'))
        return next();

        const hash = await bcrypt.hash(user.password, 10)

        user.password = hash;
        next()
    }
)

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

     return compare;
}

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

 module.exports = users;
Enter fullscreen mode Exit fullscreen mode

Create a blogModel.js file to define the blog schema.

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

const blogSchema = new Schema({
    title:{
        type: String,
        required: true,    
    },
    description:{
        type: String,
    },
    author:{
        type: String,
    },
   state:{
         type: String, 
         default: "draft", enum: ["draft", "published"]
    },
    read_count:{
        type: Number,
        default: 0
    },
    reading_time:{
        type: String
    },
    tags:{
        type: String
    },
    body:{
        type: String,
        required: true
    },
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Users',
    },   
},
    {timestamp: true}
)
const blog = mongoose.model("blog", blogSchema);

module.exports = blog;
Enter fullscreen mode Exit fullscreen mode

Create a folder Middleware. Inside the folder create a file known as userAuthenticate.js which would contain code for the authentication of users.
userAuthenticate.js

const jwt = require('jsonwebtoken');
const userModel = require("../model/userModel");
require("dotenv").config();

const userAuthenticate = async(req, res, next)=>{
    let token 

    if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')){

        try{
            token = req.headers.authorization.split(" ")[1]

            const verifiedToken = jwt.verify(token, process.env.SECRET_TOKEN)

            req.user = await userModel.findById(verifiedToken.id)

            next()

        }catch(error){
            res.status(401).send('Not authorized')
        }
    }
    if(!token){
        res.status(401).send('No token!')
    }
}

module.exports = userAuthenticate;

Enter fullscreen mode Exit fullscreen mode

For the reading_time feature in the blog, we would create an algorithm to calculate the reading time for each blog.
Create a folder known as Reading Time. A file named ReadingTime.js should be created inside.

const readingTime = (body) => {
    const wpm = 225;
    const textLength = body.trim().split(/\s+/).length;
    const minutes = Math.ceil(textLength / wpm);
    return `${minutes} minutes`;
};

module.exports = {
    readingTime
};

Enter fullscreen mode Exit fullscreen mode

CONTROLLER
Create a controller folder which would contain controllers for the user and the blog. Controllers are call back functions that handles routes requests. They separate codes used to route requests and the code used to process them. It makes code look cleaner.
Create a userController.js file.
This contains route request for the sign up and login of users.

const jwt = require('jsonwebtoken');
const userModel = require('../model/userModel');
const bcrypt = require('bcrypt');
require('dotenv').config();

// code for sign up and login of users

const userSignup = async(req, res) =>{
    const {first_name, last_name, email, password} = req.body

    if(!first_name & !last_name & !email & !password){
        return res.status(400).send({message:"Please fill in the field"})
    }

    try{
        const user = await userModel.create({
            first_name: first_name,
            last_name: last_name,
            email: email,
            password: password
        })
        res.status(200).send({message: "user created successfully!", user})
    }
    catch(error){
        res.send(error)
    }
};

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

    try{
        const user = await userModel.findOne({email})

        if (!user){
            return res.status(400).send({message:"User not found! please register"})
        }

        const validateUser = await bcrypt.compare(password, user.password)

        if(!validateUser){
            return res.status(400).send({message: "Incorrect password"})
        }

        const userId = {
            id: user._id,
            email: user.email
        }
        const token = jwt.sign(userId, process.env.SECRET_TOKEN, {expiresIn: '1h'} )
        return res.status(200).send({message: "Login successful!", token})
    }catch(error){
        res.send(error)
    }
}

module.exports = {userSignup, userLogin};
Enter fullscreen mode Exit fullscreen mode

Create a blogController.js file.
In the file several features were added to the blog such as reading time, pagination, searching of blogs by author, title and tags, ordering of blogs by read_count, reading_time and timestamp

const userModel = require('../model/userModel');
const blogModel = require('../model/blogModel');
const jwt = require("jsonwebtoken");
const {readingTime} = require('../helper/helper');
require("dotenv").config();

// Creating blog
const createBlog = async(req, res, next)=>{
    const {title, description, author, tags, body} = req.body;

        if(!title & !description & !author & !tags & !body){
            res.status(400).send({message:"Please fill in the field"})
        }
    try{
        const userBlog = await userModel.findById(req.user._id);

            const blog = new blogModel({
                title: title,
                description: description,
                author: `${userBlog.first_name} ${userBlog.last_name}`,
                tags: tags,
                body: body,
                reading_time: readingTime(body),
                user: userBlog._id,
        })

        // Saving the blog
        const saveBlog = await blog.save();

        userBlog.article = userBlog.article.concat(saveBlog._id);
        await userBlog.save();

        res.status(200).send({ message: 'Blog is created Succesfully'});
    }catch(error){
        res.status(400).send(error.message)
    }
}

// Getting all blogs created
const getBlog = async(req, res, next)=>{

        // pagination
         const page = parseInt(req.query.page) || 0;
         const limit = parseInt(req.query.limit) || 20;

         // searching blog by author
         let search = {};
        if(req.query.author){
            search = {author: req.query.author}
        }  
        // searching blog by title
        else if(req.query.title){
            search = {title: req.query.title}
        }
        // searching blog by tags
        else if(req.query.tags){
            search = {tags: req.query.tags}
        }

    try{
        const blog = await blogModel.find(search).where({ state: 'published' })
        // orderable by read_count, reading_time and timestamp
        .sort({read_count: 1, reading_time: 1, timestamp: 1})
        .skip(page*limit)
        .limit(limit)


        const count = await blogModel.countDocuments();

        if(!blog){
            res.status(404).send({message:"No blog found!"})
        }

        res.status(200).send({
                blog: blog,
                totalPages: Math.ceil(count/limit),
                currentPage: Page
        });

    }

    catch(error){
        res.status(400).send(error.message)
    }

}

//getting blogs by id
const getBlogById = async (req, res, next) => {
        const id = req.params.id;

    try {
        const blog = await blogModel.findById(id).where({ state: 'published' }).populate("user");

        if (!blog)
            return res.status(400).send({ message: 'No blog!' });

        blog.read_count++;
        const saveBlog = await blog.save();

        res.status(200).send(saveBlog);
    } catch (error) {
        res.status(400).send(error.message)
    }
};

// updating blog by id
const updateBlogById = async (req, res, next) => {
    const id = req.params.id;
    const user = req.user;
    const {title, description, state, tags, body} = req.body;

    try {

        const blog = await blogModel.findById(id);
        console.log(blog)

        if (user.id === blog.user.toString()) {
            const blogUpdate = await blogModel.findByIdAndUpdate(id, 
                {
                    $set: {
                        state: state,
                        title: title,
                        description: description,
                        body: body,
                        tags: tags,
                    },
                },
                {
                    new: true,
                }
            );

            res.status(200).send(blogUpdate);
        } else {
            res.status(400).send({ message: 'You are not authorized!' });
        }
    } catch (error) {
        res.status(400).send(error.message)
    }
};

//deleting blogs by id
const deleteBlogById = async (req, res, next) => {
    const id = req.params.id;
    const user = req.user
    try {
        const blog = await blogModel.findById(id);

        const user = await userModel.findById(id);

        if (user.id === blog.user._id.toString()) {
        const blogDelete = await blogModel.findByIdAndDelete(id);

        const index = user.article.indexOf(id);
        if(index !== -1){
            user.article.splice(index, 1);
            await user.save();

            await userBlog.save();         
        return res.status(201).send({ message: 'Deleted successfully' });
        }

        } else {
            res.status(400).send({ message: 'You are not authorized' });
        }
    } catch (error) {
        res.status(400).send(error.message)
    }
};

//getting user blog
const blogByUser = async (req, res, next) => {
    try {
        const user = req.user;
        const page = parseInt(req.query.page) || 0;
        const limit = parseInt(req.query.limit) || 20;

        // filterable by state
        let search = {};
        if(req.query.state){
            search = { state: req.query.state}
        };

        const blogUser = await userModel.findById(user.id).where(search).populate("article").skip(page*limit).limit(limit);
        const count = await blogModel.countDocuments();

        res.status(200).send({
            blogUser: blogUser.article,
            totalPages: Math.ceil(count/limit),
            currentPage: Page
         });

    } catch (error) {
        res.status(400).send(error.message)
    }
};

module.exports = {createBlog, getBlog, getBlogById, updateBlogById, deleteBlogById, blogByUser}
Enter fullscreen mode Exit fullscreen mode

Create a route folder which would contain the route url for both the user and the blog.
userRoute.js

const express = require('express');
const {userSignup, userLogin} = require('../Controller/userController');
const userRoute = express.Router();

userRoute.post("/signup", userSignup);
userRoute.post("/login", userLogin);

module.exports = userRoute;
Enter fullscreen mode Exit fullscreen mode

blogRoute.js

const express = require('express');
const blogModel = require('../model/blogModel');
const blogRoute = express.Router();
const userAuthenticate = require('../Middleware/userAuthenticate');
const {createBlog, getBlog, getBlogById ,updateBlogById, deleteBlogById, blogByUser} = require('../Controller/blogController');


blogRoute.post('/create', userAuthenticate, createBlog);
blogRoute.get('/get', getBlog);
blogRoute.get('/get/:id', getBlogById);
blogRoute.put('/update/:id', userAuthenticate, updateBlogById);
blogRoute.delete('/delete/:id', userAuthenticate, deleteBlogById);
blogRoute.get('/userblog', userAuthenticate, blogByUser)


module.exports = blogRoute;
Enter fullscreen mode Exit fullscreen mode

Then, we can test the API by running the endpoints in POSTMAN or Thunder client that is available in Vs Code.

💖 💪 🙅 🚩
lysakb
Borokinni Elizabeth

Posted on January 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related