Creating a Blog API with nodejs using express and mongodb: JWT Authentication
Published: 2023-01-08 14:09:51

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
```
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
```
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": ""
}

```
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
```

```
$gitignore
// add the .env file 
node_modules
.env
```
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;
```
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}

```
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}`)
})
```
**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;
```
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;
```
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;

```
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
};

```

**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};
```
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}
```
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;
```
**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;
```
Then, we can test the API by running the endpoints in POSTMAN or Thunder client that is available in Vs Code.