Building a Nodejs Blog API With JWT Authentication.
Table of contents
In this tutorial, we'll be showing you how to create a powerful blog API using Node.js, JWT, and Mongoose.
Our API will allow users to create, read, update, and delete blog posts, and we'll be using JWT (JSON Web Token) to secure the API and ensure that only authorized users can access certain routes. By the end of this tutorial.
Building an API can initially seem intimidating, especially if you're new to web development. But don't worry – we'll walk you through every step of the process, starting with setting up a new Node.js project and installing the required dependencies.
Once you have your project set up, we'll guide you through the process of creating a simple server using Express, the popular web framework for Node.js. We'll then show you how to connect to a MongoDB database using Mongoose, the popular object modeling library for MongoDB, and set up a schema for your blog posts.
When we are done with the database setup, we'll move on to defining routes for creating, reading, updating, and deleting blog posts. These routes will handle incoming HTTP requests and interact with the database to create, read, update, and delete documents as needed.
Finally, we'll show you how to implement authentication using JWT. We'll create routes for signing up and logging in, and we'll use bcrypt to hash the user's password and store it in the database. We'll also create a middleware function that verifies the JWT and attaches the decoded payload to the request object, which we can then use to secure our routes and ensure that only authenticated users can access certain routes.
By the end of this tutorial, you'll have a fully functional blog API. Let's get started building your very own blog API with Node.js, JWT, and Mongoose!
FUNCTIONALITY
Sign up new users
Login users
Create blog
Get a blog by id
Get all published blogs
Update blog
Delete blog
PREREQUISITES
Nodejs installed on your system,
Express.js installed on your system
A good knowledge of javascript and express.js
MongoDB to connect the app to MongoDB
SETUP
I will be using Visual Studio Code for the duration of this article, you can use any code editor of your choice. Now Let’s jump right in.
Initialize NPM:
Create a folder for the project, named mine Blog API.
Run the command on the terminal, this initializes npm and creates a package.json file in our folder.
npm init -y
Install Dependencies:
- Type the command below to install the dependencies we will be using: bcrypt, dotenv, express, jsonwebtoken, and mongoose. We also need Nodemon a development dependency to automatically restart the server as we make changes
npm i bcrypt dotenv express jsonwebtoken mongoose
npm i nodemon -D
Create A Server:
Create an index.js file in the root directory of our project, inside this file, use the Require keyword to import the express Module,
Create a constant “app” and assign an express() function to it. This creates an Express application. The express() function is a top-level function exported by the express module.
const express = require('express');
const app = express()
Next, we initialize a port, create a .env file in the root folder, open it and add PORT as an environment variable and set the value to 6000.
Import dotenv module, this module loads environment variables from the .env file into process.env,
We now listen for incoming requests on the PORT, then send back a response.
We need to set our app to use a body parser, this is how we can receive input values. We will be using the built-in middleware function in Express
index.js file.
const express = require('express');
require('dotenv').config()
const app = express()
const PORT = process.env.PORT || 6000
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get("/", (req, res) => {
res.json({ message: " Welcome to my blog" });
});
connectToDb();
app.listen(PORT, () => {
console.log(`server listening on port ${PORT}`);
});
Connect To Database:
Create a file connentToDb.js, for us to connect to the MongoDB database we need a module named mongoose
Import the mongoose module and assign it to a constant mongoose
Import the dotenv module, we need our MongoDB URL. This should be saved in the .env file
connentToDb.js file.
const mongoose = require("mongoose");
require("dotenv").config();
const MONGOOSE_URL = process.env.MONGOOSE_URL;
const connectToDb = () => {
mongoose.connect(MONGOOSE_URL);
mongoose.connection.on("connected", () => {
console.log("connected to MongoDb successfully");
});
mongoose.connection.on("error", (error) => {
console.log("An error occurred", error);
});
};
module.exports = connectToDb;
Database Schema:
Create a folder “model”
create a file inside the folder “userModel.js”
We create our user model in the “userModel.js” file
We add some properties to our schema and their associated SchemaType. For example, the property email will be cast to the String SchemaType
We then export our user model
userModel.js file
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const userSchema = new Schema(
{
email: {
type: String,
required: true,
unique: true,
},
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
blog: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "blogs",
},
],
},
{ timestamps: true }
);
const users = mongoose.model("users", userSchema);
module.exports = users;
Next, we create another file inside our model folder “blogModel.js”
Then we create our blog schema in it
Make sure to export the blog model also.
blogModel.js file.
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const blogSchema = new Schema(
{
title: {
type: String,
required: true,
unique: true,
},
description: {
type: String,
},
author: {
type: String,
required: true,
},
state: {
type: String,
required: true,
enum: ["draft", "published"],
default: "draft",
},
readCount: {
type: Number,
default: 0,
},
tags: {
type: [String],
},
body: {
type: String,
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "users",
},
},
{ timestamps: true }
);
const blogs = mongoose.model("blogs", blogSchema);
module.exports = blogs;
BLOG FUNCTIONALITY
In this section, we will be building the actual functionality of our blog, and we will also use JWT to authenticate our users. Let’s see some definitions of so term before we proceed with the codes.
What are authentication and authorization?
Authentication and authorization are separate processes used by organizations to secure their systems and data from falling into the wrong hands. Authentication and authorization are the first lines of defense against data breaches.
Authentication is the process of identifying users and making sure users are who they say they are, authentication works through usernames, passwords, biometric information, and other information entered by the user.
Authorization is the process that determines what resource an authenticated user can access.
JWT
JSON Web Token(JWT) is an open-source standard for representing claims between two parties, The token is either signed using a private secret or a public/private key. JWT claims are usually used to pass the identity of authenticated users.
Sign Up New User:
Create a “authcontroller.js” file, then import the user model, bcrypt (used for password encryption), jsonwebtoken and dotenv.
Next, we implement the signup of new users
we need to get the user's input
Validate the user input, check if the user already exists in our database
Use bcrypt to encrypt the user's password
We then create the user in our database
Lastly, we create a signed JWT token and expire it after 1-hour
authcontroller.js file
const users = require("../models/usersModel");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
require("dotenv").config();
// Define a function to create a token
const signToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1h" });
};
//SIGN UP NEW USERS
const signup = async (req, res) => {
try {
const { email, firstName, lastName, password } = req.body;
if (!(email && firstName && lastName && password)) {
return res.status(400).send("All Input Is Required");
}
const olderUser = await users.findOne({ email });
if (olderUser) {
return res.status(409).send("User Exist, Please Login. ");
}
const hashpassword = await bcrypt.hash(password, 12);
const newUser = new users({
firstName,
lastName,
email: email.toLowerCase(),
password: hashpassword,
});
const token = signToken(newUser._id);
const user = await newUser.save();
res.status(200).json({ status: "success", token, user });
} catch (error) {
res.status(500).json(error.message);
}
};
Login User:
We get user input and validate the input
Check our database to ensure the user exists
Compare the user password against the password we saved earlier in our database
Lastly, we create a signed token for our user
authcontroller.js file continues here
const login = async (req, res) => {
try {
const {email, password} = req.body
if(!(email && password)){
res.status(400).json("All input is required ")
}
const user = await users.findOne({ email: req.body.email });
if (!user) {
return res.status(400).json("Wrong Details, Try Again");
}
const match = await bcrypt.compare(req.body.password, user.password);
if (!match) {
return res.status(400).json("Wrong password, Try Again");
}
const token = signToken(user._id);
res.status(200).json({ user, token });
} catch (error) {
res.status(500).json(error.message);
}
};
module.exports = { signup, login };
Next, we create a file “blogController.js”. This file will contain all the functions that we need to do the basic CRUD operations on our Blog API.
Create Blogs:
Get user input from the request body
Get the user from our database using the ID from the request body
Create the blog using the input from the user, and save it to our database
Add the blog to an array of blogs created by the user
Send a successful message or send the error if we encounter one
A function to create a new blog.
const createBlog = async (req, res) => {
try {
const { title, description, body, tags } = req.body;
const { id } = req.user;
const user = await users.findById(id);
const author = ${user.firstName} ${user.lastName};
const newBlog = new blogs({
title,
description,
body,
author,
tags,
user: user._id,
});
const blogPost = await newBlog.save();
user.blog = user.blog.concat(blogPost._id);
await user.save();
res.status(200).json({ blogPost });
} catch (error) {
res.status(500).json(error.message);
}
};
Get Blog By Id:
Access the id of the blog from the request params
Use the find by id method and pass the id as an argument
This returns the requested blog and we send it back as a response or error if we encounter one
Function to get a blog by id
const getABlog = async (req, res) => {
try {
const id = req.params.id;
const blog = await blogs.findById(id).where({ state: "published" });
if (!blog) {
res.status(404).json("NO blog found!");
}
blog.readCount++;
await blog.save();
return res.status(200).json(blog);
} catch (error) {
return res.status(500).json(error.message);
}
};
Get All Published Blogs:
Create a copy of the request query and assign it to an object queryObj
Create an array excludedFields, this list contains query parameters to be excluded from the query
Iterate through the list and remove it from the queryObj
The query variable is then assigned the result of calling the find method on the Blogs and passing in the queryObj as an argument. This returns a list of all the blog posts matching the specified query parameters
If the sort query parameter is present in the request, we sort using the specified field else we sort using the createdAt field in descending order
Define the page variable and assign the value of the page query parameter to it, if the page query parameter is not present, we set page to 1. Repeat the same process for the limit query parameter; if it's not present, we set the limit to 20.
Define the skip variable and set it to the product of (page - 1) and limit. This calculates the number of documents to skip based on the specified page and limit values.
The page, limit, and skip variables help in pagination and limiting the number of documents returned
Finally, we retrieve all published blog posts by calling the
find()
method on Blogs and passing the query object as an argument. If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.
A function to get a list of all published blogs
const getAllBlogs = async (req, res) => {
try {
const queryObj = { ...req.query };
//FILTERING
const excludedFields = ["page", "sort", "limit", "fields"];
excludedFields.forEach((el) => delete queryObj[el]);
let query = blogs.find(queryObj);
//SORTING
if (req.query.sort) {
const sortBy = req.query.sort.split(",").join(" ");
query = query.sort(sortBy);
} else {
query = query.sort("-createdAt");
}
//PAGINATION
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
if (req.query.page) {
const numOfArticle = await blogs
.countDocuments()
.where({ state: "published" });
if (skip >= numOfArticle) throw new Error("page does not exist");
}
query = query.skip(skip).limit(limit);
const publishedBlogs = await blogs
.find(query)
.where({ state: "published" })
.populate("user", { firstName: 1, lastName: 1, _id: 1 });
res.status(200).json(publishedBlogs);
} catch (error) {
res.status(500).json(error.message);
}
}
Update Blog By Id:
Define an id variable and set it to equal req.params.id
Define a user variable and assign the user property of the req object to it
Next, destructure the req.body and assign its properties to the variables with the same name
Retrieve the blog post with the specified id by calling the findById method on the Blogs object and passing the id as an argument
Next, we compare the id property of the blog.user object to the id property of the user object. This checks that the user making the request is the owner of the blog post. If the user is the owner, we update the blog post by calling the findByIdAndUpdate() method on the Blogs object passing in the id variable, an object containing the updated data, and an option object set to true.
We save the updated blog post and send a response back with a status code of 200 and the updated blog post. If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.
A function to update a blog by id
const updateBlog = async (req, res) => {
try {
const id = req.params.id;
const user = req.user;
const { title, description, state, tags, body } = req.body;
const blog = await blogs.findById(id);
if (blog.user._id.toString() === user.id) {
try {
const updatedBlog = await blogs.findByIdAndUpdate(
id,
{ title, description, state, tags, body, readingTime },
{ new: true }
);
await updatedBlog.save();
res.status(200).json(updatedBlog);
} catch (error) {
res.status(500).json(error.message);
}
} else {
res.status(401).json("Unauthorized");
}
} catch (error) {
res.status(500).json(error.message);
}
};
Getting Personal Blogs:
Define the variable userid and set it to the id property of the user object in the req object
Define the page variable and set it to the value of the page query parameter. If the page query parameter is not present, the page variable is set to 1
Define the limit variable and set it to the value of the limit query parameter. If the limit query parameter is not present, the limit variable is set to 20
Define the skip variable and set it to the product of (page - 1) and limit. This calculates the number of pages to skip based on the specified page and limit value
Retrieve all blog posts by the specified user by calling the find() method on the Blogs object passing an object with the user field set to the userid variable as an argument
Send a response back with a status code of 200 and the list of the user’s blog posts. If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.
A function to get back personal blogs
const getUserBlogs = async (req, res) => {
try {
const userid = req.user.id;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
const userBlogs = await blogs
.find({ user: userid })
.skip(skip)
.limit(limit);
return res.status(200).json(userBlogs);
} catch (error) {
return res.status(500).json(error.message);
}
};
Delete a Blog By Id:
Define an id variable and set it to equal req.params.id
Define a user variable and assign the user property of the req object to it
Retrieve the blog post with the specified id by calling the findById method on the Blogs object and passing the id as an argument
Next, we compare the id property of the blog.user object to the id property of the user object. This checks that the user making the request is the owner of the blog post
Delete the blog post by calling the deleteone() method on the Blogs object
Send a response with a status code of 200 and a message indicating that the blog was deleted successfully
If the user is not the owner of the blog post, send a JSON response with a status code of 401 and the string “unauthorized”
If an error occurs during the execution of the code, we send a status code of 500, and the error message back as a response.
A function to delete blog by id
const deleteBlog = async (req, res) => {
try {
const id = req.params.id;
const user = req.user;
const blog = await blogs.findById(id);
if (blog.user._id.toString() === user.id) {
try {
await blogs.deleteOne();
res
.status(200)
.json({ status: "success", message: "blog deleted successfully" });
} catch (error) {
res.status(500).json(error);
}
} else {
res.status(401).json("Unauthorized");
}
} catch (error) {
res.status(500).json(error);
}
};
Next, we export all the functions
module.exports = {
createBlog,
getABlog,
getAllBlogs,
updateBlog,
getUserBlogs,
deleteBlog,
};
Authentication Middleware:
We need a middleware function to protect routes that require authentication. The function checks the authorization header of the request to see if it contains a valid JWT token, retrieves the user associated with the token, and then assigns the user to the req object. This allows the subsequent route handler function to have access to the authenticated user. If the authorization header is not present or the token is invalid, the function sends a response to the client indicating that the user is unauthorized.
authmiddleware.js
const jwt = require("jsonwebtoken");
const users = require("../models/usersModel");
//PROTECTING ROUTES
const isAuthenticated = async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
token = req.headers.authorization.split(" ")[1];
}
if (!token) {
return res.status(401).json("Unauthorized!. Please login");
}
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
const user = await users.findById(decodedToken.id);
if (!user) {
return res.status(401).json("authorization not found");
}
req.user = user;
next();
};
module.exports = isAuthenticated;
Routing:
Next, we handle our routing
blogroute.js
const blogRouter = require("express").Router();
const isAuthenticated = require("../middleware/authmiddleware.js");
const blogcontroller = require("../controller/blogcontroller");
blogRouter
.route("/")
.get(blogcontroller.getAllBlogs)
.post(isAuthenticated, blogcontroller.createBlog);
blogRouter.route("/:id").get(blogcontroller.getABlog);
blogRouter
.route("/owner/:id")
.get(isAuthenticated, blogcontroller.getUserBlogs)
.put(isAuthenticated, blogcontroller.updateBlog)
.delete(isAuthenticated, blogcontroller.deleteBlog);
module.exports = blogRouter;
authroute.js
const authcontroller = require("../controller/authcontroller");
const userRouter = require("express").Router();
userRouter.route("/signup").post(authcontroller.signup);
userRouter.route("/login").post(authcontroller.login);
module.exports = userRouter;