Creating a blog API with Node.js, Express.js and MongoDB
A step by step process to building a blogging API
Photo by Fotis Fotopoulos on Unsplash
In this article, we will build a blog API step by step, from initialization to implementation and testing. The functionalities of this blog API will include CRUD (i.e. Creating a blog post, Reading a blog post, Updating a blog post and Deleting a blog post), Validation(user validation and blog validation), and Testing.
Prerequisite
Knowledge of Node.js, Express.js and MongoDB
Setting Up
To start this project, you have to create a new folder on your desktop that will include your js files and folders subsequently
Set up a Database
create two MongoDB databases (main and test)on your local MongoDB server (MongoDB compass) or in the cloud (MongoDB Atlas)
copy the connection string and paste it into your environment variable (.env) file.
Create a package.JSON file, run the node package manager
npm init
and install the dependencies from the root directory by doingnpm install <module name>
.Create a .env file which is the environment variable to keep some critical info like USER_PASSWORD, MongoDB URL and some other vital info
Install nodemon
npm install nodemon
to keep your app running after every saves. For example, runnodemon server.js
After setting up our project locally, then we start creating files and folders
Note: Kindly create a .gitignore file to keep files like node_module and .env untracked by Git.
Here's a list of some essential files;
app.js
server.js
blogdb.js
blog_model.js
user_model.js
auth_router.js
user_router.js
blog_router.js
user_controller.js
blogValidation.js
userValidation.js
app.js
The app.js is the base of our code, where we must import some inbuilt modules like express, passport, rate limiter, etc., as required in our code. It is also necessary to add the routes, middlewares and global error handling. Here's what my app.js look like
const express = require("express");
const passport = require("passport");
const helmet = require("helmet");
const rateLimiter = require("express-rate-limit");
const authRouter = require("./routes/auth_router");
const blogRoute = require("./routes/blog_router");
const userRoute = require("./routes/user_router");
require("./middleware/passport");
const app = express();
// Rate Limitter
const limiter = rateLimiter({
windowMs: 1 * 60 * 1000,
max: 4,
standardHeaders: true,
legacyHeaders: false,
});
// middleware
app.use(passport.initialize());
app.use(express.json());
app.use(express.urlencoded());
app.use(helmet);
app.use(limiter);
app.use("/", authRouter);
app.use("/blogs", blogRoute);
app.use("/users", userRoute);
app.get("/", (req, res) => {
res.send("welcome to homepage");
});
app.use((err, req, res, next) => {
const status = err.status || 500;
const message = err.message.message || " error in server";
return res.status(status).json({ status: "something went wrong", message });
});
module.exports = app;
server.js
This is where our server runs by listening to our PORT from our environment variable file, and then we console.log() the callback function to be sure that the server is properly running. So the purpose of having our server running separately is that when we are testing our endpoints when our server is running, we can also use the same server for testing.
require("dotenv").config();
const connectToMongoDB = require("./blogdb");
const app = require("./app");
const PORT = process.env.PORT || 4000
// const localHost = "127.0.0.1"
connectToMongoDB();
app.listen(PORT, () => {
console.log("Listening on port", PORT);
});
blogdb.js
Here we require mongoose and our environment variable file to use the CONNECTION_URL, so on connection, we have a callback function that outputs a success message, and if there's an error, we also console it and then export it to use in our app.
const mongoose = require('mongoose');
require('dotenv').config();
const CONNECTION_URL = process.env.pizza_Connection_Url
mongoose.connect(CONNECTION_URL)
function connectToMongodb(){
mongoose.connection.on("connected", () => {
console.log("Connected to MongoDB Successfully");
});
mongoose.connection.on("error", (err) => {
console.log("An error occurred while connecting to MongoDB");
console.log(err);
});
}
module.exports = connectToMongodb
MODELS
After ensuring our app is running perfectly and connected to the database, we can now create models for both the blog and the user. In these files, we have our mongoose, brycpt and validator required.
blog_model.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const ObjectId = mongoose.Schema.ObjectId;
const blogSchema = new Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
trim: true,
},
author_id: {
type: ObjectId,
ref: "user",
},
state: {
type: String,
enum: ["published", "draft"],
default: "draft",
},
read_count: {
type: Number,
default: 0,
},
reading_time: { type: String },
tags: [String],
body: {
type: String,
trim: true,
required: [true, "you blog must have a body"],
},
timestamp: {
type: Date,
default: Date.now(),
},
});
const Blog = mongoose.model("Blog", blogSchema);
module.exports = Blog;
user_model.js
const bcrypt = require("bcrypt");
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;
const userSchema = new Schema({
email: {
type: String,
lowercase: true,
require: [true, " A user must have a email"],
trim: true,
unique: true,
},
first_name: {
type: String,
minLength: 2,
require: true,
},
last_name: {
type: String,
minLength: 2,
require: true,
},
password: {
type: String,
minLength: 2,
require: true,
trim: true,
require: [true, "kindly enter your password"],
},
createdAt: {
type: Date,
default: Date.now(),
},
});
userSchema.pre("save", async function (next) {
// Only run this function if password was actually modified
if (!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 12);
// Delete passwordConfirm field
this.confirmPassword = undefined;
next();
});
userSchema.methods.correctPassword = async function (
candidatePassword,
userPassword
) {
return await bcrypt.compare(candidatePassword, userPassword);
};
const user = mongoose.model("user", userSchema);
module.exports = user;
CONTROLLERS
In this folder, we are going to create two files
blogController.js
In the blogController.js file, we import the blogModel and other various functions like createBlog,getAllBlogs, getBlogById, updateBlogs, and DeleteBlog
const blogModel = require("../model/blog_model");
exports.createBlog = async function (req, res, next) {
try {
const blogBody = req.body;
const blogReadingTime = function () {
const blogLenght =
blogBody.body.split(" ").length +
blogBody.title.split(" ").length +
blogBody.description.split(" ").length;
console.log(blogLenght);
const totalReadingtime = blogLenght / 200;
return `${totalReadingtime} minute`;
};
console.log(req.user)
// console.log({...req.body, author: req.user._id })
blogBody.reading_time = blogReadingTime();
let newBlog = await blogModel.create({ ...req.body,author_id:req.user._id })
console.log(newBlog)
return res.status(201).json({
status: "success",
newBlog,
});
} catch (error) {
next(error);
}
};
exports.getAllBlogs = async function (req, res, next) {
try {
const filterBlog = { state: "published" };
const queryObj = { ...req.query };
// console.log(queryObj);
let blogQuery = blogModel.find(filterBlog);
//sorting read_count, reading_time and timestamp
if (req.query.sort) {
const searchBy = req.query.sort.split(",").join(" ");
blogQuery = blogQuery.sort(searchBy);
} else {
blogQuery.sort();
}
//pagination
const page = +req.query.page || 1;
const limit = +req.query.limit || 20;
const skip = (page - 1) * limit;
blogQuery.skip(skip).limit(limit);
const Blogs = await blogQuery;
res.status(200).json({ status: "success", blogList: Blogs.length, Blogs });
} catch (error) {
next(error);
}
};
exports.ownerBlog = async function (req, res, next) {
console.log(req.user);
try {
const Blog = await blogModel.find({ author: req.user._id });
console.log(Blog);
return res.status(200).json({ status: "success", Blog });
} catch (error) {
next(error);
}
};
exports.getBlogById = async function (req, res, next) {
try {
const { blogId } = req.params;
console.log(blogId);
// console.log(blogId);
let Blog = await blogModel.findById(blogId).populate("author_id");
console.log(Blog);
if (!Blog) {
res.status(404);
const error = new Error("Can't find blog with this ID");
return next(error);
}
Blog.read_count += 1;
await Blog.save({ validateBeforeSave: true });
res.status(200).json({ status: "success", blogList: Blog.length, Blog });
} catch (error) {
next(error);
}
};
exports.updateBlog = async function (req, res, next) {
try {
const { blogId } = req.params;
const { state } = req.body;
const blogToBeUpadated = await blogModel.findById(blogId);
console.log(blogToBeUpadated)
if (!blogToBeUpadated) {
return next(new Error("Sorry, this is not your blog"));
}
console.log(blogToBeUpadated.author_id,req.user._id)
if (blogToBeUpadated.author_id.toString() !== req.user._id) {
return next(new Error("Sorry, you are not authorized to update this blog "));
}
const updatedBlog = await blogModel.findByIdAndUpdate(blogId, state, {
new: true,
runValidators: true,
});
console.log(updatedBlog);
if (!updatedBlog) {
res.status(404);
const error = new Error("Can't find blog with this ID");
next(error);
}
console.log(updatedBlog.author_id,req.user._id)
if (updatedBlog.author_id.toString() !== req.user._id) {
return next(
new Error("Sorry, you are not authorized to update this blog")
);
}
updatedBlog.state = state;
await updatedBlog.save();
return res.status(200).json({ status: "success", updatedBlog });
} catch (error) {
next(error);
}
};
exports.deleteblog = async function (req, res, next) {
try {
const { blogId } = req.params;
const blogToBeDeleted = await blogModel.findById(blogId);
if (!blogToBeDeleted) {
return next(new Error("Sorry,this is not your blog"));
}
if (blogToBeDeleted.author_id.toString() !== req.user._id) {
return next(new Error("Sorry, you are not authorized to delete this blog "));
}
const Blog = await blogModel.findByIdAndDelete(blogId);
console.log(Blog);
if (!Blog) {
res.status(404);
const error = new Error("Can't find blog with this ID");
return next(error);
}
return res.status(204).json({ messsge: "deleted successfully" });
} catch (error) {
next(error);
}
};
userController.js
In the userController, we also import similar functions like getAllUser, getUserById, updateUser, deleteUser and the userModel.
const userModel = require("../model/user_model");
exports.getAllUsers = async function (req, res, next) {
try {
const Users = await userModel.find();
return res
.status(200)
.json({ message: "success", result: Users.length, Users });
} catch (error) {
next(error);
}
};
exports.getUserById = async function (req, res, ) {
try {
const { userId } = req.params;
console.log(userId);
const user = await userModel.findById(userId);
console.log(user);
if (!user) {
return res.status(404).json({ status: false, order: null });
}
return res.json({ status: true, user });
} catch (error) {
return res.status(404).json({ message: "Sorry, can't get user by id", error });
}
};
exports.updateUser = async function (req, res, ) {
try {
const { userId } = req.params;
console.log (userId);
const userDetails = req.body;
const user = userModel.findById(userId);
console.log(user);
if (!user) {
return res.status(404).json({ status: false, order: null });
}
await userModel.findByIdAndUpdate(userId, userDetails, {
runValidators: true, // Runs validations for the updated fields
});
return res.json({ status: true });
} catch (error) {
return res.status(404).json({ message: "user update is unsuccessful", error });
}
};
exports.deleteUser = async function (req, res, ) {
try {
const { userId } = req.params;
// console.log (userId);
const user = await userModel.findById(userId);
console.log(user._id);
if (!user) {
return res.status(404).json({ status: false, order: null });
}
// console.log(user);
await userModel.deleteOne(user, {
runValidators: true, // Runs validations for the updated fields
});
return res.json({ status: true });
} catch (error) {
return res.status(404).json({ message: "user update is unsuccessful", error });
}
};
VALIDATORS
Apart from using mongoose for validating our user input, we can also use the third-party library called JOI to validate user input. JOI has a way of sending a friendly error message to the user when user inputs are wrong. You can check more on JOI here. In this folder, we have two files.
blogValidation.js
const joi = require("joi");
const blogValidationMiddleware = async (req, res, next) => {
try {
await blogSchema.validateAsync(req.body);
next();
} catch (error) {
console.log("err", error);
return res.status(406).send(error.details[0].message);
}
};
const blogSchema = joi.object({
title: joi.string().min(2).max(30).required(),
description: joi.string().min(10).max(250).optional().trim(),
author: joi.ref("user"),
state: joi.array().has(["published,draft"]).default("draft"),
read_count: joi.number().default(0),
reading_time: joi.string(),
tags: joi.string(),
body: joi.string().trim().required(),
timestamp: joi.date().default(Date.now),
});
module.exports = blogValidationMiddleware;
userValidation.js
const joi = require("joi");
joiObjectId = require("joi-objectid")(joi);
const userValidatorMiddleWear = async (req, res, next) => {
try {
userValidator.validateAsync(req.user);
next();
} catch (error) {
res.status(406).send(error.details[0].message);
}
};
const userValidator = joi.object({
email: joi
.string()
.email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } })
.required(),
first_name: joi.string().min(2).required(),
last_name: joi.string().min(2).required(),
password: joi.string().min(2).trim().required(),
confirmPassword: joi.ref("password"),
});
module.exports = userValidatorMiddleWear;
MIDDLEWARE
In this folder, we have the passport.js file where we will require JSON web token, passport, passport-jwt and our userModel. The JWT strategy gets user input and signs by sending a token to the user. We also have the signup and sign-in function where the user is authenticated using the JSON web token strategy to sign user input.
const passport = require('passport');
const jwt = require('jsonwebtoken');
const userModel = require('../model/user_model');
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
require('dotenv').config();
passport.use(
new JWTstrategy(
{
secretOrKey: process.env.JWT_SECRET,
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
},
async (payload, done) => {
try {
return done(null, payload.user);
} catch (error) {
done(error);
}
}
)
);
// SIGNUP CONTROLLER ROUTE USING THE FORM_ ENCODED
//sign token function
const signToken = (user) => {
return jwt.sign({ user }, process.env.JWT_SECRET, {
expiresIn: process.env.EXPIRATION_TIME,
});
};
exports.signup = async function (req, res, next) {
try {
const { email, first_name, last_name, password } = req.body;
const user = await userModel.create({
email,
first_name,
last_name,
password,
});
user.password = undefined;
const token = signToken(user);
return res.json({
message: "Signup successfull",
user,
token,
});
} catch (error) {
next(error);
}
};
exports.login = async function (req, res, next) {
try {
const { email,password } = req.body;
if (!email || !password ) {
const error = new Error("Username or password is incorrect");
return next(error);
}
// check if user exist && password is correct
const user = await userModel.findOne({ email });
console.log(user);
if (!user || !(await user.correctPassword(password, user.password))) {
const error = new Error("Incorrect email or password");
return next(error);
}
// if all the conditions are meet send token
user.password = undefined;
const token = signToken(user);
res.status(200).json({
status: "success",
token,
});
} catch (error) {
return next(error, { message: "login was not successful try Again!!!" });
}
};
TEST
Testing our endpoints is the last thing we do after building our API to ensure that our app is working correctly, as we have variables for our API in our .env file. We also have dummy variables and a dummy server for testing our API. In our test folder, we have two files; blog.test.js and user.test.js. For testing our endpoints, we will be using jest and supertest to run our test, so we have to install jest and supertest i.e. npm install jest supertest
After installing it then, we require the supertest in our test files.
blog.test.js
Here we will require our blogModel, supertest, app, mongoose and dummy variables from our .env in our blog.test.js. We will connect to our test database to test our API. To make our testing more effortless, it is better to test for the response given by each endpoint.
const { JsonWebTokenError } = require("jsonwebtoken");
const mongoose = require("mongoose");
const supertest = require("supertest");
require("dotenv").config();
const blog_test_url = process.env.blog_test_url;
const app = require("../app");
const TEST_TOKEN = process.env.TEST_TOKEN;
jest.setTimeout(40000);
beforeAll((done) => {
mongoose.connect(blog_test_url);
mongoose.connection.on("connected", async () => {
console.log("connected to Mongodb successfully");
});
mongoose.connection.on("error", (err) => {
console.log(err);
console.log("An error occured");
});
done();
});
afterAll((done) => {
mongoose.connection.close(() => done());
});
// //BLOG ROUTE TESTING
let blogId
test("POST blog",async()=>{
const blog_body = {
title: "adeola's blog",
tags: "lifestyle",
body: "my new blog",
author: "adeola",
description: "my first blog",
};
const response = await supertest(app)
.post("/blogs")
.set("Authorization", `Bearer ${TEST_TOKEN}`)
.send(blog_body);
blogId = response.body.newBlog._id;
expect(response.status).toBe(201);
expect(response.body.newBlog.title).toBe("adeola's blog");
expect(response.body.newBlog.state).toBe("draft");
})
test("GET blog",async()=>{
const response = await supertest(app)
.get("/blogs")
.set("authrization", `Bearer ${TEST_TOKEN}`)
expect(response.status).toBe(200);
expect(response.body.status).toBe("success")
})
test("GET blog?id",async()=>{
const response = await supertest(app)
.get(`/blogs/${blogId}`)
.set("Authorization", `Bearer ${TEST_TOKEN}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty("Blog");
})
user.test.js
const mongoose = require("mongoose");
const supertest = require("supertest");
require("dotenv").config();
const blog_test_url = process.env.blog_test_url;
const app = require("../app");
const TEST_TOKEN = process.env.TEST_TOKEN;
jest.setTimeout(40000);
beforeAll((done) => {
mongoose.connect(blog_test_url);
mongoose.connection.on("connected", async () => {
console.log("connected to Mongodb successfully");
});
mongoose.connection.on("error", (err) => {
console.log(err);
console.log("An error occured");
});
done();
});
afterAll((done) => {
mongoose.connection.close(() => done());
});
let userId
test("POST user",async()=>{
const userData = {
first_name: "adeola",
last_name: "aderemi",
email: "demo@gmail.com",
password: "qwerty",
};
const response = await supertest(app)
.post("/signup")
.set("content-type", "application/x-www-form-urlencoded")
.send(userData);
userId = response.body.user._id;
console.log(userId);
expect(response.status).toBe(200);
expect(response.body.message).toBe("Signup successfull");
expect(response.body).toHaveProperty("user");
})
test("GET user",async()=>{
const response = await supertest(app)
.get("/users")
.set("Authorization", `Bearer ${TEST_TOKEN}`);
expect(response.status).toBe(200);
expect(response.body.message).toBe("success");
})
test("get userByID",async()=>{
const response = await supertest(app)
.get(`/users/${userId}`)
.set("Authorization", `Bearer ${TEST_TOKEN}`)
expect(response.status).toBe(200);
expect(response.body.status).toBe(JSON.parse("true"));
expect(response.body).toHaveProperty("user")
})
test("update userByID",async()=>{
const userData= {
email: "adeola@gmail.come",
};
const response = await supertest(app)
.patch(`/users/${userId}`)
.set("Authorization", `Bearer ${TEST_TOKEN}`)
.send(userData);
expect(response.status).toBe(200);
expect(response.body.status).toBe(JSON.parse("true"));
})
test("delete userByID",async()=>{
const response = await supertest(app)
.delete(`/users/${userId}`)
.set("Authorization", `Bearer ${TEST_TOKEN}`);
expect(response.status).toBe(200);
expect(response.body.status).toBe(JSON.parse("true"));
})
Now that our API is functional, we can push it to GitHub, and you can add more functionalities later if you like. Thank you, and I hope you found this article beneficial. Kindly drop a comment if you have any feedback.
P.S. This is my first technical article. I'll do better. You can check out my other writeups here and connect with me here. Thank you again.