How I built,deployed and improved a simple Blog API using Express, JWT, NodeJS and MongoDB

Table of contents

No heading

No headings in the article.

I recently built a simple Blog API as part of my learning experience. In the first iteration, I was able to meet all the basic requirements set out for the project. Please note that this is not a tutorial in the strictest sense but a midlevel view of the tasks and processes I undertook to create this project.

First, what were these requirements? Here they are:

  1. Users should have a first name, last name, email, and password.

  2. A user should be able to sign up and sign into the blog app.

  3. Use JWT as an authentication strategy and expire the token after 1 hour.

  4. A blog can be in two states; draft and published.

  5. Logged-in and not logged-in users should be able to get a list of published blogs created.

  6. Logged-in and not logged-in users should be able to get a published blog.

  7. Logged-in users should be able to create a blog.

  8. When a blog is created, it is in a draft state.

  9. The owner of the blog should be able to update the state of the blog to published.

  10. The owner of a blog should be able to edit the blog in draft or published state.

  11. The owner of the blog should be able to delete the blog in draft or published state.

  12. The owner of the blog should be able to get a list of their blogs.

    1. The endpoint should be paginated

    2. It should be filterable by state

  13. Blogs created should have a title, description, tags, author, timestamp, state, read_count, reading_time and body.

  14. The list of blogs endpoint that can be accessed by both logged-in and not logged in users should be paginated,

    1. default it to 20 blogs per page.

    2. It should also be searchable by author, title and tags.

    3. It should also be orderable by read_count, reading_time and timestamp.

  15. When a single blog is requested, the API should return the user information(the author) with the blog. The read_count of the blog too should be updated by 1.

  16. Come up with an algorithm for calculating the reading time of the blog.

  17. Write tests for all endpoints.

First Iteration

I started by first thinking through my design approach, I wanted to build this as simple as possible and then bolt on more complex features later on.

I chose to use a straightforward MVC paradigm and incorporate middleware wherever I thought it was necessary. Here is a look at the directory structure:

Next I created a couple of files for starting a server and connecting to a database. Note that for this project I used Express and MongoDB. Here is a look at the code for index.js which is essentially the file that knits everything together.

Here is the app.js file

You will notice that my routes, environment variable and database files are self-contained and were only imported where I needed to use them.

Next, I built out my models for data and connected them to controllers for manipulation and business logic. In this project I needed to set up an authentication and authorization system and a data store of blog articles. This meant that I needed a blog model and a user model. Let's take a look at the user model.

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const { isEmail } = require('validator');


const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;

const UserSchema = new Schema({
    id: ObjectId,
    created_at: Date,
    firstname: {
        type: String,
        required: [true, 'Please enter a first name']
    },
    lastname: {
        type: String,
        required: [true, 'Please enter a last name']
    },
    email: {
        type: String,
        unique: true,
        lowercase: true,
        required: [true, 'Please enter an email'],
        validate: [isEmail, 'Please enter a valid email']
    },
    password: {
        type: String,
        required: [true, 'Please enter a password']
    }
});

UserSchema.post('save', (doc, next) => {

    next();
});

UserSchema.pre('save', async function(next) {
    const hashedpassword = await bcrypt.hash(this.password, 10);
    this.password = hashedpassword;
    next();
})

const User = mongoose.model('User', UserSchema);
module.exports = User;

I used hooks and error handling in the schema with Mongoose, again because I wanted this to be as simple as possible. Next, let us look at the blog schema

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;

const BlogPostSchema = new Schema({
    id: ObjectId,
    timestamp: { type: Date },
    title: { type: String, lowercase: true, required: [true, 'Blog must have a title'] },
    description: { type: String },
    tags: [],
    author: { type: String, required: [true, 'Blog must have a author'] },
    read_count: { type: Number },
    read_time: { type: Number },
    body: { type: String, required: [true, 'Blog must have a body'] },
    state: { type: String, enum: ['draft', 'published'], default: 'draft' },


});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);
module.exports = BlogPost;

With my models all done I moved on to my controllers. I wanted a user to have the ability to sign up and log in so I set up two controllers accordingly. Here they are:

const handleErrors = require('../services/auth.services');
const UserModel = require('../models/user.model')
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

//controller for sign up route
module.exports.signup = async(req, res) => {
    try {
        const { firstname, lastname, email, password } = req.body;
        const user = await UserModel.create({ firstname, lastname, email, password });
        if (user) {
            return res.status(201).json({ message: "Successfully created user", user: user });
        }
        return res.status(400).json({ message: "Something went wrong" });
    } catch (error) {
        const errors = handleErrors(error);
        return res.status(409).json(errors);
    }
};
module.exports.login = async(req, res) => {
    try {
        const { email, password } = req.body;
        let token;
        //validate user data
        if (!email || !password) {
            return res.status(400).json({ message: "Incomplete Input" });
        }
       const user = await UserModel.findOne({ email: email });
        if (!user) {
            return res.status(400).json({ message: "Wrong email" });
        }
        if (user && await (bcrypt.compare(password, user.password))) {
            token = await jwt.sign({
                    userid: user._id,
                    email: user.email,
                    firstname: user.firstname,
                    lastname: user.lastname
                },
                process.env.jwt_secret, { expiresIn: process.env.expiry });
        }
        return res.status(200).json({ token: token });
    } catch (error) {
        return res.status(409).json({ message: "An error occurred" + error.message });
        console.log(error);
    }

};

I used the JWT module for creating and verifying tokens and used the bcrypt module for hashing passwords. I used a similar process for building out the blog controllers. These were a little more complex as I had to filter and query based on specified criteria. Here is a look at the code:

const router = require('express').Router();
const BlogModel = require('../models/blog.model');
const UserModel = require('../models/user.model');
const moment = require('moment');
const { getWordCount, getTags, getUserIdFromToken } = require('../services/blog.services')



//create post
module.exports.createBlogPost = async(req, res) => {
    try {
        const { title, description, tags, body } = req.body;

        const read_count = 0;
        let sanitized_tags = !tags ? ["untagged post"] : getTags(tags);
        sanitized_tags.push("post");
        const read_time = getWordCount(body) * process.env.TIMEPERWORD;

        const payload = {
                timestamp: moment().toDate(),
                title: title.toLowerCase(),
                description,
                tags: sanitized_tags,
                author: getUserIdFromToken(req.headers.token),
                read_count,
                read_time,
                body,
                state: 'draft'

            }
            // //validation of content


        if (!title || !body) {
            return res.status(409).json({ message: "Please enter a title and content" });
        }

        const blogpost = await BlogModel.create(payload);
        if (blogpost) {
            return res.status(200).json({ message: "successfully posted", blogpost: blogpost })
        }

        return res.status(500).json({ message: "We encountered a problem creating the blog post, please try again" });
    } catch (error) {
        return res.status(409).json({ message: "An error occurred: " + error.message });
        console.log(error);
    }

};
//get all published posts: point 5
module.exports.getPublishedPosts = async(req, res) => {

    const { page, posts, author, title, tags, order_by, order } = req.query;
    const searchQuery = { state: 'published' },
        sortQuery = {};

    let searchtags = [];
    //Pagination
    const startpage = (!page ? 0 : page);
    const postsPerPage = (!posts ? 20 : posts);
    const sortOrder = (!order ? "asc" : order);
    const orderParams = (!order_by ? "timestamp" : order_by);
    //Searching
    searchtags = (!tags ? ["post"] : getTags(tags));
    if (author) {
        searchQuery.author = author;

    }

    if (title) {
        searchQuery.title = title.toLowerCase();
    }

    //Sorting

    sortParams = orderParams.split(",");
    for (const param of sortParams) {

        if (sortOrder == "asc" && order_by) {
            sortQuery[param] = 1;
        }
        if (sortOrder == "desc" && order_by) {
            sortQuery[param] = -1;
        }
        if (sortOrder == "desc" && !order_by) {
            sortQuery[param] = -1;
        }
        if (sortOrder == "asc" && !order_by) {
            sortQuery[param] = 1;
        }

    }



    const blog = await BlogModel
        .find({ tags: { $in: searchtags }, ...searchQuery })
        .sort(sortQuery)
        .skip(startpage)
        .limit(postsPerPage);


    if (!blog) {
        res.status(401).json({ message: "Sorry , there are currently no published posts" });
        res.end();
    }
    if (blog.length === 0) {
        res.status(409).json({ message: "Sorry , there are currently no published posts that match your search criteria" });
        res.end();
    } else {
        return res.status(200).json({ blog });
    }

};
//get published post by id: point 6
module.exports.getPublishedPost = async(req, res) => {
    try {
        const { id } = req.params;
        const blog = await BlogModel.findById(id);

        if (!blog) {
            return res.status(404).json({ message: "Sorry , this post was not found." });
        }
        const user = await UserModel.findById(blog.author);
        blog.read_count++;
        await blog.save()
        return res.status(200).json({ blog, user });
    } catch (err) {
        return res.status(404).json({ message: "Please check post id and try again" });
    }
};
//update a post state
module.exports.updatePostState = async(req, res) => {
    const { id } = req.params;
    const userId = getUserIdFromToken(req.headers.token);
    const blog = await BlogModel.findById(id);
    if (!blog) {
        return res.status(404).json({ status: false, message: "Post does not exist" })
    }
    if (blog.author === userId) {
        blog.state = 'published';
        await blog.save();
        return res.status(200).json({ message: "Post state update successfully to published", blog: blog });
    }

    return res.status(403).json({ message: "Unauthorized to update this post" });





};
//update a post
module.exports.updatePost = async(req, res) => {
    try {
        const { id } = req.params;
        const { title, description, tags, body } = req.body;


        if (!title || !body) {
            return res.status(409).json({ message: "Please enter a title and content" });
        }
        if (!description || !tags) {
            return res.status(409).json({ message: "Please enter a description and tags" });
        }
        let sanitized_tags = getTags(tags);
        sanitized_tags.push("post");
        const userId = getUserIdFromToken(req.headers.token);
        const blog = await BlogModel.findById(id);
        if (!blog) {
            return res.status(404).json({ status: false, message: "Post does not exist" })
        }

        if (blog.author === userId) {
            blog.title = title;
            blog.description = description;
            blog.tags = sanitized_tags;
            blog.body = body;
            blog.read_time = getWordCount(body) * process.env.TIMEPERWORD;
            await blog.save();
            return res.status(200).json({ message: "updated successfully", blog: blog });
        }

        return res.status(403).json({ message: "Unauthorized to update this post" });

    } catch (err) {
        return res.status(403).json({ message: err });
    }



};
//delete a post
module.exports.deletePost = async(req, res) => {
    const { id } = req.params;
    const userId = getUserIdFromToken(req.headers.token);
    const blog = await BlogModel.findById(id);



    if (!blog) {
        return res.status(404).json({ status: false, message: "Post does not exist" })
    }

    if (blog.author === userId) {

        const post = await BlogModel.deleteOne({ _id: id })

        return res.json({ status: true, message: "Post deleted successfully" })
    }

};

module.exports.getUserPosts = async(req, res) => {
    const {
        page,
        posts,
        state,
        order
    } = req.query;
    const searchQuery = {};

    //Pagination
    const startpage = (!page ? 0 : page);
    const postsPerPage = (!posts ? 10 : posts);
    const sortOrder = (!order ? "asc" : order);
    const stateParam = (!state ? "draft" : state);

    //Searching
    searchQuery.author = getUserIdFromToken(req.headers.token);
    searchQuery.state = stateParam;


    //Sorting

    const blog = await BlogModel.find(searchQuery)
        .sort(sortOrder)
        .skip(startpage)
        .limit(postsPerPage);
    if (!blog) {
        return res.status(404).json({ status: false, message: "This user does not have any posts" })
    }

    return res.status(200).json({ blog: blog });
};

Now that I was done with the basics, I needed to create routes to test my work so far. This is a straightforward process of creating routes and specifying what controller handled the request on each route. I created three separate route files as follows:

Home Routes: For index requests

const router = require('express').Router();

router.post('*', (req, res) => {
    res.status(404).send("Not found");
});

router.get('*', (req, res) => {

    res.status(404).send("Not found");
});

module.exports = router;

Auth Routes: For authentication and authorization

const router = require('express').Router();
const authController = require('../controllers/auth.controllers');

router.post('/signup', authController.signup);
router.post('/login', authController.login);
module.exports = router;

Blog Routes: For articles

const router = require('express').Router();
const blogController = require('../controllers/blog.controllers');
const { isAuthorized } = require('../services/blog.services')


//create post
router.post('/create', isAuthorized, blogController.createBlogPost);

//get particular post by id
router.get('/published_post/:id', blogController.getPublishedPost);

//get published posts
router.get('/published_posts', blogController.getPublishedPosts)

//update particular post state
router.patch('/updatePostState/:id', isAuthorized, blogController.updatePostState);

//update particular post
router.patch('/editPost/:id', isAuthorized, blogController.updatePost);

//delete particular post
router.delete('/deletePost/:id', isAuthorized, blogController.deletePost);

//get particular user's posts
router.get('/userPosts', isAuthorized, blogController.getUserPosts);


module.exports = router;