Creating A Blog REST API Using NodeJS, ExpressJs, MongoDB, And Mongoose

Creating A Blog REST API Using NodeJS, ExpressJs, MongoDB, And Mongoose

Introduction

Hello reader,

My name is Itohowo Monday Umoh, I am a backend developer. I will be showing you how to create a REST blog API using Nodejs, Expressjs, MongoDB and Mongoose. I'm super excited so let's get started.

REST (REpresentational State Transfer) API (Application Programming Interface) is a software architectural style that defines the set of rules to be used for creating web services. Web services which follow the REST architectural style are known as REST web services. It allows requesting systems to access and manipulate web resources by using a uniform and predefined set of rules meaning allows two applications to communicate with each other over the internet and through various devices.

Prerequisite

  1. Node

  2. MongoDB

  3. Express

  4. Mongoose

  5. Postman

  6. Nodemon

  7. Passport

  8. Jsonwebtoken

  9. bcrypt

  10. Dotenv

  11. body-parser

Install node

If you do not have Node installed, here is a step-by-step process on how to install it.

getting-started-with-nodejs

Install Express

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. if you do not have Express installed run this command on your terminal.

npm install express

Install Mongoose

Mongoose provides a straightforward, schema-based solution to model your application data. so basically Mongoose is a way to make a connection with the MongoDB database. if you do not have Mongoose installed run this command on your terminal.

npm install mongoose

Install Nodemon

Nodemon is a tool that helps develop Node.js-based applications by automatically restarting the node application when file changes in the directory are detected. run this command on your terminal to install nodemon.

npm install nodemon

Postman

Postman is a tool you can use to interact with an API.

how to install postman

Passport, Body-parser, Jsonwebtoken , Bcrypt and Dotenv

some tools used for authentication and authorization and parsing body data.

npm i passport jsonwebtoken bcrypt dotenv body-parser

Let's Code

we are going to create a blog REST API where a user can read published articles, also create or write their article also update and delete their articles. so basically the user is performing CRUD(create, read, update delete) operations.

Initializing Our Project

The first thing you do when you want to start a node project is to run this command on the terminal.

npm init -y

Add Boilerplate

let's get our server up and running so we can get requests and send responses or so the Client- side can communicate with the server-side. we create a file index.js.

index.js:

const express = require("express");
const bodyParser = require('body-parser')

const app = express()
const port = 3000;

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.get('/', (req,res)=>{
    console.log('welcome to my blog')
    return res.json({ status: true })
});

app.use('*', (req, res, err) => {
    return res.status(404).send({ message: ' not found' })
})

// start server
app.listen(port, ()=>{
    console.log(`server started at localhost:${port}`)
});

So the first thing we did is import express, and then we created two variables app and port their respectful values are the express function and HTTP port.

we then use the app function to start the server.

so when the server starts running we can now send requests and responses, so the first request we would be sending is a GET method request that returns or respond { status: true }. the next request is handling a bad request which is when a user enters the wrong route.

bodyparser is Node.js middleware that parses incoming request bodies in a middleware before your handlers, available under the req.body property.

Now let's see if our server would run

nodemon index.js

Connect database

Our server is running let's connect to MongoDB using mongoose, we can do that by creating a new file name db.js.

db.js:

const mongoose = require("mongoose");

let url = process.env.DATABASE_URL ||  "mongodb://localhost:27017/students-record";

mongoose.connect(url, {
    useCreateIndex: true,
    useNewUrlParser: true,
    useUnifiedTopology: true
}).then(() => {
    console.log("Connected to MongoDB Successfully")
}).catch((e) => {
    console.log("An error occurred while connecting to MongoDB");
    console.log(err);
})

when connecting to a MongoDB database the first thing we do is import mongoose. Mongoose has methods it can use to connect to a database as you could see above.

Then we need to call the connect method with the help of mongoose where we pass URL and a call back function that returns a promise. To avoid deprecation Warnings set useCreateIndex: true, useNewUrlParser: true, useUnifiedTopology: true

We need to require db.js in our express file (index.js) require('./db.js')

const express = require("express");
const bodyParser = require('body-parser')


require('./db'); //here 
const app = express()
const port = 3000;

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.get('/', (req,res)=>{
    console.log('welcome to my blog')
    return res.json({ status: true })
});

app.use('*', (req, res, err) => {
    return res.status(404).send({ message: ' not found' })
})

// start server
app.listen(port, ()=>{
    console.log(`server started at localhost:${port}`)
});

Defining Schema

I believe at this point our server is running and our database has successfully been connected now let's define our database schemas(A database schema is the skeleton structure that represents the logical view of the entire database.)

We are going to have two schemas, the user model and blog model. create a folder name models and in that folder, we would have two files user.model.js and blog.model.js

In user.model.js:

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

const Schema = mongoose.Schema;

const UserSchema = new Schema({
    email: {
        type: String,
        required: true,
        unique: true
    },
    first_name: {
        type: String
    },
    last_name: {
        type: String
    },
    password: {
        type: String,
        required: true
    }
});

UserSchema.pre(
    'save',
    async function(next) {
        const user = this;
        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('users', UserSchema);

module.exports = UserModel;

Once again we start by importing mongoose, this time we are importing something else bcrypt.

Then we created a variable UserSchema which is an object of the user constraint or model, which are email, first_name, last_name and password.

then the UserSchema.pre() is a pre-hook that hash the user's password before saving it in the database.

The isValidPassword function validates the user password during login.

lastly, we export the users model

In blog.model.js :

const mongoose = require('mongoose');


const Schema =  mongoose.Schema;


const BlogSchema = new Schema ({
    blogInfo : {
    title: {
        type: String,
        require: true,
        unique: true
    },
    description:{
        type: String,
        require: true,
    },

    body : {
        type: String,
        require: true
    },
    tags:{
        type: String, enum: ['tech', 'sport', 'international','entertainment', 'others'],
        require: true
   }
},
    author: {
        type: mongoose.Types.ObjectId, 
        ref: "users"
    },
    state:{
        type: String,
        default: "draft",
        require: true 
    },
    read_count: {
        type: Number
    },
    read_time:{
        type: String
    },
    timestamp : {
        type: Date,
        default: Date.now
    }
})




module.exports = mongoose.model('blog',BlogSchema );

Same process as the user model just different constraints (title, description, body, tags, author, state, read_count, read_time and timestamp.

while most of their value will be strings or default. author constraint value will be a user object that is the user who created the blog.

Creating Routes

API Routes a specific path to take to get specific information or data. there are going to be 3 routes on this API, user route, auth route and blog route.

Auth route :

Authentication and authorization are important parts of building APIs because you want to know if the user is who he/she says they are and then give permissions to these authenticated users.

So let's get right into routing, haha, the first thing we do is to create a routes folder then we create a file auth.route.js

auth.route.js :

const express = require('express');
const  Passport  = require('passport');
const JWT = require('jsonwebtoken');

require('dotenv').config();


authrouter.post(
    '/signup',
    Passport.authenticate('signup', {session: false}), async(req, res, next) =>{
        res.status(201).json({
            message: 'signup successful',
            user: req.user
        });
    }

);

authrouter.post(
    '/login',
    async (req, res, next) =>{
        Passport.authenticate('login', async ( error, user, info) =>{
            try{
                if(error) {
                    return next(error)
                }
                if(!user){
                    const error = new Error('email or password is incorrect');
                    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({ token })
                }); 
            } catch(error){
                return next(error);
            }
        }
        )(req, res, next);
    }
);

We are going to be using passport and jsonwebtoken (JWT) for authentication. In the auth route we have two path /signup and /login.

On the /signup path we have a passport function that authenticates the user signup data and if the user gets authenticated it returns message: 'signup successful'.

On the /login path passport authenticates the user's login details (email and password) if the details are correct the user gets an authorization token.

using passport and jwt for authentication

Before we move on to the next route we need to update our codebase because at the moment there are some missing variables. create a file and name it .env

.env:

JWT_SECRET = &edued92duhh6qwgdbcu
  • The above is simply an environmental variable that we probably want to use anywhere on the codebase. we used the Dotenv module to import this variable from the .env file to anywhere in the code base.

  • User route :

  • the user route is meant for users to create their profile, user updating their profile and view their profile and that of others.

  • So to do this we create a file in the routes folder and name it user.route.js

  • user.route.js:

const express = require("express");

const userController = require("../controllers/user.controller")

const userRouter = express.Router()

userRouter.get('/', userController.getAllUser);
userRouter.post('/', userController.createNewUser);
userRouter.patch('/:userId', userController.updateUserDetails);
userRouter.delete('/:id', userController.deleteUser);

module.exports = userRouter

we start by importing express and an object of functions we are going to create. these functions are the reason the various path can carry out their objectives they are called controllers. we use the express router function to create the respective path using the HTTP verb and then we call the function we want to run in that path.

For more understanding of http verb.

blog route :

Just like the user route we want to create paths that users can use to create blog posts, publish the blog post, edit or update their blog posts and delete their blog post. create a blog.route.js in the routes folder.

blog.route.js:

const express = require("express");
const passport = require('passport');

const blogController = require('../controllers/blog.Controllers');
const middleware = require('../middleware/blog.middleware');

const blogRouter = express.Router()

// get all blogs
blogRouter.get('/',blogController.countMiddleware,blogController.getAllBlogs );

// get one blog
blogRouter.get('/:id',blogController.countMiddleware,blogController.getOneBlog );

//create blog
blogRouter.post('/:authorId',passport.authenticate('jwt', { session: false }),blogValidator,blogController.createBlog );

// publish a blog
blogRouter.patch('/publishblog/:id', passport.authenticate('jwt', { session: false }),middleware.confirmBlogAuthor , blogController.publishBlog);

// update a blog
blogRouter.patch('/:id',passport.authenticate('jwt', { session: false }), middleware.confirmBlogAuthor, blogController.updateBlog);

// delete a blog
blogRouter.delete('/:id',passport.authenticate('jwt', { session: false }), middleware.confirmBlogAuthor, blogController.deleteBlog);

We imported express for routing and passport for protecting this path, why because we want to ensure only login users can create, update and delete blog post. also, we have blogController an object of functions we are going to create to make the paths carry out their purpose.

Now we have all our routes set up let's export them to our index.js.

index.js:

const express = require("express");
const passport = require('passport');
const bodyParser = require('body-parser')

require('./db');

const app = express()
const port = 3000;


const authRouter = require("./routes/auth.route")
const blogRouter = require('./routes/blog.route');
const userRouter = require('./routes/user.route')

// authentication route
app.use('/', authRouter);

// blogs route
app.use('/blogs', blogRouter); 

//users route
app.use('/users',passport.authenticate('jwt', { session: false }), userRouter);

app.get('/', (req,res)=>{
    console.log('welcome to my blog')
    return res.json({ status: true })
});

app.use('*', (req, res, err) => {
    return res.status(404).send({ message: ' not found' })
})

// start server
app.listen(port, ()=>{
    console.log(`server started at localhost:${port}`)
});

Creating controllers

Each path in our routes has controllers, that's a function that runs when it is triggered. so since we have three routes, we would also have three controllers. auth controller, user controller and blog controller.

auth controller :

the first thing we are going to do is to install a few dependencies. these are modules that are needed to perform authentication and authorization. run the command below.

npm install passport-jwt passport-local

the next thing we do is to create a controller folder then we create a file auth.controller.js

auth.route.js:

const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;

const UserModel = require('../models/user.models');

passport.use(
    new JWTstrategy (
        {
            secretOrKey:process.env.JWT_SECRET,
            jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken() 
        },
        async(token, done) =>{
            try{
                return done(null, token.user);
            } catch(error) {
                done(error);
            }
        }
    )
);

passport.use(
    'signup',
    new localStrategy(
        {
            usernameField: 'email',
            passwordField: 'password'
        },
        async (email, password, done) =>{
            try {
                const user = await UserModel.create({email, password});

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

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

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

            const validate = await user.isValidPassword(password);

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

            return done(null, user, { message: `${user} your logged in` })
        } catch (error){
            return done(error)
        }
    }
    )
);

we start by importing dependencies as passport, localStrategy, JWTstrategy and ExtractJWT. then we use passport to create a function that will extract secret_token for authorization. we use JWTstrategy to create a strategy for the extraction of tokens and ExtractJWT to extract the token from the authorization header.

In the next block, we use passport again to create a signup function and localStrategy to create a strategy for signup. the strategy use userModel to create a new user. That's basically what signup is for creating or adding a new user.

In the last block, that's where we perform login. we are using passport and localStrategy again to create a login function and strategy. so the function is trying to authenticate that the user is who he says he is through his input. so if the email exists in the database and the password is correct then the user will be logged in and a token will be sent back to the user for authorization.

user controller:

the next controller on our API is the user controller. so we create a file user.controller.js in the controller folder.

user.controller.js:

const UserModel = require("../models/user.models")

const updateUserDetails = (req, res) =>{
    const userUpdate = req.body
    const id = req.params.userId

    UserModel.findByIdAndUpdate(id, userUpdate)
        .then(userUpdate => {
            res.status(202)
            res.json({
                message: "your details have successfully been updated",
                data: userUpdate
            })
        } )
        .catch(err => {
            res.status(500)
            res.json({
                message: 'user not updated something went wrong',
                data: err
            })
        })
}

const deleteUser = (req, res) =>{
    const Id = req.params.id

    UserModel.findByIdAndDelete(Id)
    .then(() =>{
        res.status(202)
        res.send({
            message: "deleted sucessfully"
        })
    })
    .catch((err) =>{
        res.status(500)
        res.send({
            message: "An error occured user not deleted",
            data : err
        })
    })
}

const getAllUser = (req, res) =>{
    UserModel.find({})
    .then((users) =>{
        res.status(200)
        res.send(users)
    })
    .catch((err) =>{
        res.status(404)
        res.send({
            message: "no user found",
            data : err
        })
    })
}

module.exports = {
    getAllUser,
    updateUserDetails,
    deleteUser
}

Here we start by importing the user model. then we create a function called updateUserDetails. the function uses the user model to find and update user data in the database. the next function deleteUser uses the user model to find by id and delete the user. the last function getAllUser finds and return all the user in the database.

blog controller :

the final controller is the blog controller. first, we create a blog.controller.js file in controller folder.

blog.controller.js:

const blogModel = require('../models/blog.Models')

let count=0;
// update readcouount function
async function countMiddleware(req,res,next){ 
    await blogModel.updateMany({}, { $inc: { read_count: +1 }} )
     if(next){

        next()
     }else{
        res.send('something went wrong')
     }
}

const getAllBlogs =  async (req, res) =>{

    const page = parseInt(req.query.page);
    const order = req.query.order;
    const limit = 20;


    if (order === 'asc'){
        mySort = {
            readTime: 1,
            readCount: 1,
            timestamp: 1
        }
    }

    if (order === 'desc'){
        mySort = {
            readTime: 0,
            readCount: 0,
            timestamp: 0
        }
    }
       const startIndex = (page-1)*limit;

    await blogModel.find({state : "published"})
    .populate({path : "author", model: "users"})
    .limit(limit)
    .sort(order)
    .skip(startIndex)
    .then(blog => {
            res.status(200)
            res.send(blog)

    } ).catch(err => {
        res.status(404)
        res.send({
            message: "no blog(s) was found",
            data: err
        })
    })
};

const getOneBlog = async (req, res) =>{
    const Id = req.params.id

    await blogModel.findById(Id).populate({path : "author", model: "users"})
    .then(blog =>{
        if ({state : 'published'}){

        res.status(200)
        res.send(blog)
        console.log(state)
        }else{
            res.status(404)
            res.send({
                message: "this blog has not been published yet",
            })
        }

    }).catch(err => {
        res.status(404)
        res.send({
            message: " blog not  found",
            data: err
        })
    })


}

// function to create new blog
const createBlog = (req,res) =>{
    const blogData = req.body;

    const authorId =  req.params.authorId;


    // // // author value
    const author = authorId;


    // calculating for readtime
    const blogBody = blogData.blogInfo.body
    const blogBodyLenght = blogBody.length

    readTime = Math.round(blogBodyLenght/4%60)

    blogModel.create({blogInfo: blogData.blogInfo,
        author:author , read_time: `${readTime} minutes`, read_count: count
    })
        .then(blogData =>{
            res.status(201)
            res.send(blogData)


        }).catch(err =>{
        res.status(500)
        console.log(err)
        res.send({
            message: "An error occured blog  not posted",
            data : err
        })
    })
}

// delete blog function
const deleteBlog = (req, res) =>{
    const Id = req.params.id

    blogModel.findByIdAndDelete(Id)
    .then(() =>{
        res.status(202)
        res.send({
            message: "deleted sucessfully"
        })
    })
    .catch((err) =>{
        res.status(500)
        res.send({
            message: "An error occured blog  not deleted",
            data : err
        })
    })
}

// update blog function
const updateBlog = (req, res) =>{
    const blogId = req.params.id
    const blogUpdates = req.body;

    blogModel.findByIdAndUpdate(blogId, blogUpdates)
    .then(blog =>{
        res.status(202);
        res.send({
            message: "blog has successfully been updated",
            data : blog
        })
    })
    .catch((err) =>{
        res.status(500)
        res.send({
            message: "An error occured blog  not updated",
            data : err
        })
    })
}

const  publishBlog =  (req, res) =>{
    const id = req.params.id

     blogModel.findByIdAndUpdate(id, {state : "published"})
    .then((blog) =>{
        res.status(202).send({
            message: "blog stated has sucessfully been updated",

        })
    })
    .catch((err) =>{
        res.status(500).send({
            message: "An error occured blog  not updated",
            data : err
        })
    })
}

module.exports = {
    getAllBlogs,
    getOneBlog,
    createBlog,
    deleteBlog,
    updateBlog,
    publishBlog,
    countMiddleware
}

in the blog.controller.js file, we start by importing the blog model. next is a middleware countMiddleware it simply updates the value of the readCount of the blog in the database. middleware provides tools developers can use to create, expose and manage APIs for their applications.

The next function is a controller that gets all the blogs that have been published in the database just like Hashnode you can only read a blog article that has been published. also on the getAllBlogs the user can get the blog in ascending or descending order, the API is paginated and each page has a limit of twenty blogs.

the next function or controller is the getOneBlog in this controller, the user can choose to find or get one particular blog using its id.

After that, we have the createBlog function. the function makes it possible for the user to create a blog by simply inputting the blog title, description, body and tag. the inputs are gotten from req.body.

the next controller is the deleteBlog like every other controller it finds the blog it wants to delete by id and removes it from the database.

In a situation the author of the blog made a mistake and wants to correct it, that's what our next controller is for. updateBlog finds a blog by its id and updates it. the updates are gotten from the req.body.

the last controller publishBlog makes it possible for the author to publish their blog article. it uses the same method with updateBlog controller, find by id then it updates the state of the blog from 'draft' which is the default state to 'published'. then we export or controller so we can use be able to use them on our routes.

you may be wondering can any user just update and delete another user's blog? the answer is no. that's why we need a middleware to help us manage that. so create a folder middleware and in the middleware folder create a file blog.middleware.js

blog.middleware.js :

const blogModel = require('../models/blog.Models');// blog model


async function confirmBlogAuthor(req, res, next){
    const id = req.params.id;
    const author = req.user._id 
    const authorToString = author.toString()
    console.log(req.user)

   await blogModel.findById(id)
    .then(blog =>{
        const blogAuthor = blog.author
        const stringfyBlogAuthor = blogAuthor.toString()
        if(authorToString === stringfyBlogAuthor){
             next()
        }
    }).catch((err) => {
        res.status(401).send({
            message: "your not the author this blog",
            data : err
        })
    })

}


module.exports = {
    confirmBlogAuthor
}

how the middleware work is that it uses the req.user.id value and compare with id the user enters in the params if they are equal then that's the blog author. then we export the middleware to our blog route.

If you followed down to this point congratulation you have successfully created a blog restful API. I hope you found the article helpful. thanks for reading you can follow me on Twitter and ask me any question.

itohowo monday umoh