DEV Community

Cover image for Blog API using Express, JWT and MongoDB
Barny victor
Barny victor

Posted on

Blog API using Express, JWT and MongoDB

Introduction 
In this article, we are going to create a blog API using Node.js, Express, JWT and MongoDB, It includes features such as authentication so that only signed-in users can create, edit, delete and update Post, time taken to read a particular Post and number of people who read a particular Post.

Before we start here are the basic requirements you should have installed in your system:

Things you must know beforehand:

  • Basic JavaScript syntax(ES6 included)
  • Node.js

Project Setup
First, we create a folder called Blog-API on our desktop, Open the folder using the terminal, and Then in the terminal type npm init y to create a package.json file.

After that open, the project using any code editor of your choice, in your code editor install these packages in the terminal before proceeding.

npm I express mongoose jsonwebtoken dotenv bcrypt nodemon 
Enter fullscreen mode Exit fullscreen mode

When the Packages are done installing create a new file called "app.js", inside this file require an express Module, and create a constant ‘app’ for creating an instance of the express module.

const express = require('express')

const app = express()
Enter fullscreen mode Exit fullscreen mode

To receive input values, we add the middleware function "express.json()"

const express = require('express')

const app = express()

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

Enter fullscreen mode Exit fullscreen mode

In the Blog-API folder create another file called ".env", inside the env file add the MongoDB link address to the ".env" file.

MONGODB_URI://user:password@cluster0.hex8l.mongodb.net/name-database?retryWrites=true&w=majority

Enter fullscreen mode Exit fullscreen mode

The link provided in the ".env" would be used to connect to our Database later.

Inside the app.js file we would import the dotenv dependency and also create our server.

// importing dependencies
const express = require('express')
require('dotenv').config() ;

//App Config
const app = express()
const port = process.env.PORT || 5000

//Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }))

//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))

Enter fullscreen mode Exit fullscreen mode

Database Connection
Create a database user in MongoDB, grant network access, and use dotenv to connect it to the initial routes because we've saved our link in the .env file. Create a folder inside of Blog-API and call it config inside the config folder create a file named dbConfig.js.

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

const dbConnection = async () => {
    const mongodbURL =  process.env.MONGODB_URI;
    try {
        //connecting to the database
        await mongoose.connect(mongodbURL);
        console.log('MongoDB Connected...');
    } catch (err) {
        console.error(err.message);
        process.exit(1);
    }
};

module.exports = { dbConnection };
Enter fullscreen mode Exit fullscreen mode

We have to go back to the app.js file to call the database config function so that we connect to our MongoDB DATABASE.

// importing dependencies
const express = require('express')
require('dotenv').config();
const { dbConnection } = require('./config/dbConfig');

//App Config
const app = express()
const port = process.env.PORT || 5000

//Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }))

//Listener
app.listen(port, async () => {
    await dbConnection();
    console.log(`Listening on localhost: ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Database Schema and Routes
The MongoDB database stores data in JSON format rather than the regular table structure found in traditional databases like MySQL. You create the schema file that MongoDB requires. It describes how fields in MongoDB are stored.

First, make a folder called models. For this project we are going to Create two file inside the folder and name it 'UserModel.js.' and 'BlogModel.js.' UserModel.js. is where we will write our authentication schema and BlogModel.js. is where we will write our Blog schema.

UserModel
We will begin by developing the UserModel for users who register on the site. The user information would be saved in the database so that when the user returns later, they will simply log in because the server will recognize them based on the information saved.

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

const UserSchema = new mongoose.Schema(
    {
        First_Name: {
            type: String,
            required: 'First Name is required',
        },
        Last_Name: {
            type: String,
            required: 'Last Name is required',
        },
        Email: {
            type: String,
            unique: true,
            required: 'Email is required',
            match: [
                /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
                'Please add a valid E-mail',
            ],
        },
        Username: {
            type: String,
            unique: true,
            required: 'UserName is required',
        },
        Password: {
            type: String,
            required: 'Password is required',
            minlength: [6, 'Password must be at least 6 characters long'],
        },
        article: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'BLOG',
            },
        ],
    },
    { timestamps: true }
);
UserSchema.pre('save', function (next) {
    const user = this;

    if (!user.isModified('Password')) return next();

    bcrypt.hash(user.Password, 12, (err, hash) => {
        if (err) return next(err);
        user.Password = hash;
        next();
    });
});

UserSchema.methods.validatePassword = 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;
Enter fullscreen mode Exit fullscreen mode

In addition, as seen in the code above, in the last line, we exported our code in order to import it into the controller.

As you can see, the schema contains details such as Username, email, and password that will be stored in the database. When a user attempts to log in, the server will check to see if the user exists in the database and will allow the user if the user details are in the database.

BlogModel
As with almost any file or variable in this tutorial, you are free to use whatever names you want. Next, we create the Blog model in which we will store all that we want to be in our Blog. Open the BlogModel.js and type this

const mongoose = require('mongoose');

const { Schema } = mongoose;

const BlogSchema = new Schema(
    {
        user: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Users',
        },
        Title: {
            type: String,
            require: true,
            unique: true,
        },
        Description: { type: String, require: true },
        Author: { type: String, require: true },
        State: { type: String, default: 'draft', enum: ['draft', 'published'] },
        Read_Count: { type: Number, default: 0 },
        Reading_Time: { type: String },
        Tags: { type: [String] },
        Body: { type: String, require: true },
    },
    { timestamps: true }
);

const blogModel = mongoose.model('BLOG', BlogSchema);

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

as we did for the UserModel we will export the blogModel to import it into the controller.

Controller
We have our model for both Users and Blogs that will be added to our MongoDB database. we would start with the Authentication controller first before moving to the Blog controller. First, create a folder called controller inside the controller folder we create a file called AuthController.js

const UserModel = require('../Model/UserModel');
const Jwt = require('jsonwebtoken');
require('dotenv').config();

//@desc Register new user
//@route POST /register
//@access Public

const registerUser = async (req, res, next) => {
    const { email, password, firstname, lastname, username, confirmPassword } =
        req.body;
    //    making sure all fields are valid
    if (
        !email ||
        !password ||
        !firstname ||
        !lastname ||
        !username ||
        !confirmPassword
    ) {
        return res.status(400).send({ message: 'Fill empty fields' });
    }
    //   confirming password
    if (password !== confirmPassword) {
        return res.status(400).send({ message: 'PassWord must Match' });
    }

    //   saving to database
    try {
        const user = await UserModel.create({
            Email: email,
            Password: password,
            First_Name: firstname,
            Last_Name: lastname,
            Username: username,
        });
        return res.status(200).send({ message: 'User created successfully' });
    } catch (error) {
        next(error);
    }
};

//@desc Login user
//@route POST /login
//@access Public

const login = async (req, res, next) => {
    const { email, Password } = req.body;

    if (!email || !Password) {
        return res
            .status(400)
            .send({ message: 'Pls Complete the required fields' });
    }

    try {
        const user = await UserModel.findOne({ Email: email });

        if (!user)
            return res.status(409).send({ message: 'Wrong credentials!' });

        const validPassword = await user.validatePassword(Password);

        if (!validPassword)
            return res.status(401).send({ message: 'Invalid password' });

        const payload = {
            id: user._id,
            username: user.Username,
        };

        const token = Jwt.sign(payload, process.env.JWT_SECRET, {
            expiresIn: '1h',
        });
        res.cookie('accessToken', token, {
            httpOnly: true,
        }).send({
            Token: token,
            Email: user.Email,
            Name: `${user.First_Name} ${user.Last_Name}`,
        });
    } catch (error) {
        next(error);
    }
};

const logout = async (req, res) => {
    res.cookie('accessToken', '', { maxAge: 1 });
    res.send('Logged out successfully...');
};

module.exports = { registerUser, login, logout };
Enter fullscreen mode Exit fullscreen mode

POST a request to the /register endpoint. To MongoDB, the load is in req.body. Then you send the User information using the Create method to save the data in MongoDB. If it is successful, you will be given status 200; otherwise, you will be given status 500.

To get the data from the database, create a post endpoint to /login.
You're using findOne() here and getting a success code of 200. (otherwise, status 500). we used bcryptjs in the code to protect our passwords.

Next, we would create another file inside the controller called BlogController.js and write this code.

// importing  blog model from model
const blogModel = require('../Model/BlogModel');
const UserModel = require('../Model/UserModel');
require('dotenv').config();

//@desc Create a new Blog
//@route POST /
//@access Private

const createNewblog = async (req, res, next) => {
    const { title, description, body, tags } = req.body;

    if (!title || !description || !body || !tags) {
        return res.status(400).send({ message: 'Fill empty fields' });
    }
    try {
        const user = await UserModel.findById(req.user._id);

        // sending the created object to database
        const note = new blogModel({
            Title: title,
            Description: description,
            Author: `${user.First_Name} ${user.Last_Name}`,
            Body: body,
            Tags: tags,
            Reading_Time: readingTime(body),
            user: user._id,
        });

        const savedNote = await note.save();

        // saving the blog id in user shema
        user.article = user.article.concat(savedNote._id);
        await user.save();

        res.status(201).send({ message: 'Blog created Succesfully ' });
    } catch (error) {
        next(error);
    }
};

//@desc get All Blogs
//@route GET /
//@access Public

const getAllBlogs = async (req, res, next) => {
    try {
        //pagination
        const page = parseInt(req.query.page) || 0;
        const limit = parseInt(req.query.limit) || 20;

        // search by title, author and tags
        let search = {};
        if (req.query.author) {
            search = { Author: req.query.author };
        } else if (req.query.title) {
            search = { Title: req.query.title };
        } else if (req.query.tag) {
            search = { Tags: req.query.tag };
        }

        // getting al blogs from the database
        const blogs = await blogModel
            .find(search)
            .where({ State: 'published' })
            .sort({ Reading_Time: 1, Read_Count: 1, timestamps: -1 })
            .skip(page * limit)
            .limit(limit);

        const count = await blogModel.countDocuments();

        if (blogs) {
            res.status(200).send({
                message: blogs,
                totalPages: Math.ceil(count / limit),
                currentPage: page,
            });
        } else {
            res.status(404).send({ message: 'No Blog Found' });
        }
    } catch (error) {
        next(error);
    }
};

//@desc get a single blog
//@route GET /:id
//@access Public

const getSingleBlog = async (req, res, next) => {
    try {
        const singleBlog = await blogModel
            .findById(req.params.id)
            .where({ State: 'published' })
            .populate('user', { First_Name: 1, Last_Name: 1, Email: 1 });

        if (!singleBlog)
            return res.status(404).send({ message: 'No such blog found' });

        singleBlog.Read_Count++;
        const blog = await singleBlog.save();

        res.status(200).send({ blog: blog });
    } catch (error) {
        next(error);
    }
};

//@desc update Blog post by User
//@route PUT /:id
//@access Private

const upadetBlogbyUser = async (req, res, next) => {
    const { State, title, description, body, tags } = req.body;
    try {
        const user = req.user;

        const blog = await blogModel.findById(req.params.id);

        if (user.id === blog.user._id.toString()) {
            const updatedBlog = await blogModel.findByIdAndUpdate(
                { _id: req.params.id },
                {
                    $set: {
                        State: State,
                        Title: title,
                        Description: description,
                        Body: body,
                        tags: tags,
                    },
                },
                {
                    new: true,
                }
            );

            res.status(200).send(updatedBlog);
        } else {
            res.status(401).send({ message: 'You cant access this resource' });
        }
    } catch (error) {
        next(error);
    }
};

//@desc delete Blog post by User
//@route DELETE /:id
//@access Private
const deleteBlogByUser = async (req, res, next) => {
    try {
        const user = req.user;

        const blog = await blogModel.findById(req.params.id);

        if (user.id === blog.user._id.toString()) {
            await blogModel.findByIdAndDelete(req.params.id);
            const user = await UserModel.findById(req.user._id);

            const index = user.article.indexOf(req.params.id);
            if (index !== -1) {
                user.article.splice(index, 1);
                await user.save();
                res.status(204).send({ message: 'Deleted successfully' });
            }
        } else {
            res.status(401).send({ message: 'You cant access this resource' });
        }
    } catch (error) {
        next(error);
    }
};

//@desc get Blogs post by User
//@route GET /userarticle
//@access Private

const userBlogs = async (req, res, next) => {
    try {
        const user = req.user;

        let search = {};
        if (req.query.state) {
            search = { State: req.query.state };
        }

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

        const User = await UserModel.findById(user.id)
            .populate('article')
            .skip(page * limit)
            .where(search)
            .limit(limit);
        const count = await UserModel.countDocuments();

        res.status(200).send({
            message: 'Your blog post',
            blogs: User.article,
            totalPages: Math.ceil(count / limit),
            currentPage: page,
        });
    } catch (error) {
        next(error);
    }
};

module.exports = {
    createNewblog,
    getAllBlogs,
    getSingleBlog,
    deleteBlogByUser,
    upadetBlogbyUser,
    userBlogs,
};
Enter fullscreen mode Exit fullscreen mode

As we did with the UserModel we use The POST method to send the data we got from req.body to the database when saving the blog in the database the default state is draft we use the update method to update the state of a particular blog to published, To get all the blogs we've in the database we use the GET method alongside the find() Method in MongoDB Note we can only get a blog post that was published, To Update a blog we use the PUT method alongside with findByIdAndUpdate() of MongoDB to find the particular blog we want to update, and use findByIdAndDelete() to delete a particular blog post. to get the
time taken to read a blog post we write this code.

// calculating the total time to read the book
const readingTime = (body) => {
    const wpm = 225;
    const text = body.trim().split(/\s+/).length;
    const time = Math.ceil(text / wpm);
    return `${time} mins`;
};
Enter fullscreen mode Exit fullscreen mode

we pass the Body as a parameter in the function and use the trim() method to remove whitespaces, get the length of words in the body, and divide the length of words by our wpm(words per minute) to get the time taken to read the blog post. To get the read count for a blog for every time we visit that particular Blog page we add 1 to the read count, so each time anyone makes an API call to that blog post read count adds 1

read_Count++
Enter fullscreen mode Exit fullscreen mode

For users to get the blog post they created while creating the blog we save the blog id created in articles in the user model so
that when we use the get method to make the call we populate the articles with the blogs that a particular user created. we also add pagination and limits the data we get from the database.

Routes
Our app now knows what to do when it receives a new blogPost (controller) and what it should look like (model). But it doesn’t yet know when to do it. For this we need to define some routes. we create two files called blogRoute.js and userRoute.js and write the following code.

const express = require('express');
const registerationRoute = express.Router();
const {
    registerUser,
    login,
    logout,
} = require('../controllers/registrationController');

registerationRoute.post('/register', registerUser);
registerationRoute.post('/login', login);
registerationRoute.get('/logout', logout);

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

First, we need to import express. We need to do this so that we can call the.Router() method to create three new router objects in just a little bit. Next, we import the controller module we just created. we would do the same thing for the blogRoute

// importing xpress authenticate middleware and BlogControllers
const express = require('express');
const {
    createNewblog,
    getAllBlogs,
    getSingleBlog,
    deleteBlogByUser,
    upadetBlogbyUser,
    userBlogs,
} = require('../controllers/blogController');

const { authenticate } = require('../middleware/authenticateUser');

const blogRoute = express.Router();

blogRoute.route('/').post(authenticate, createNewblog).get(getAllBlogs);

blogRoute.route('/userarticle').get(authenticate, userBlogs);

blogRoute
    .route('/:id')
    .get(getSingleBlog)
    .put(authenticate, upadetBlogbyUser)
    .delete(authenticate, deleteBlogByUser);

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

Export both route files created into our app.js so we can use them as middleware inside the app.js

const registerationRoute = require('./routes/RegisterRoute');
const blogRoute = require('./routes/blogRoutes');

// middleware for the Routes
app.use('/api/v1/auth', registerationRoute);
app.use('/api/v1/articles', blogRoute);

Enter fullscreen mode Exit fullscreen mode

Middleware
Now we need to set up the middleware for our API routes to authenticate the User visiting the blog route.

const UserModel = require('../Model/UserModel');
const Jwt = require('jsonwebtoken');
require('dotenv').config();

const authenticate = async (req, res, next) => {
    const token = req.cookies.accessToken;

    try {
        if (!token)
            return res.status(403).send({
                message: 'No token, authorization denied',
            });

        if (token === null || token === undefined) {
            return res.status(403).send({ message: 'no token provided' });
        }

        // verify token
        if (token) {
            const user = Jwt.verify(token, process.env.JWT_SECRET);
            req.user = await UserModel.findById(user.id);
            next();
        } else {
        }
    } catch (error) {
        res.clearCookie('accessToken');
        next(error.message);
    }
};

module.exports = { authenticate };
Enter fullscreen mode Exit fullscreen mode

we export the middleware function and import it to our user Route file so as to call the function inside the user Route.

And with that, we can now start testing our endpoints to check if our code works properly you can use Postman, insomnia or Thunder client to test these endpoints. Thank you for reading the article. I hope you learned a lot from it.

Conclusion
In this article, we learnt how to create a Restful API for a blog using Express, Node.js, Jwt-Token and MongoDB.

Top comments (0)