Creating a Blog API with nodejs using express and mongodb: JWT Authentication
Borokinni Elizabeth
Posted on January 8, 2023
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.
Posted on January 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024