Creating a Blogging App API with Nodejs
Hello there, I'm going to take you through implementing a simple blogging app API with Nodejs in this article. We will learn how to use the basic CRUD operations effectively to create a seamless backend application. We will cover a lot of new concepts alongside.
About Project
At the end of this article, our application should be able to do the following:
Register new users
Log in users to their account
Creation of new blog post (only logged-in users can do this)
Delete and Update the blog
Allow users to read blog posts without requiring a login
Prerequisites
Before going ahead with this article, you should have basic knowledge of the following for a better understanding:
Nodejs
MongoDB (Download locally here)
Express JS
Project Setup
Let us start by initializing a new Nodejs project. First, we initialize a new package.json file with npm
npm init -y
Then we install the following packages with npm to get running:
npm i express bcrypt cors dotenv joi jsonwebtoken mongoose moment
npm i -D nodemon
Below is a run-through of the following packages installed:
express (HTTP server)
bcrypt (hashing / encrypting user's passwords before saving them to the database)
cors (for handling)
joi (schema validation)
jsonwebtoken (user authorization)
mongoose (for interacting with MongoDB database)
moment (for formatting dates)
Building our Application
To get started we will create an new file app.js
in the root directory. Add the following lines of code there:
app.js
const express = require("express");;
const cors = require("cors")
require("dotenv").config();
const app = express();
app.use(cors())
app.use(express.json());
app.get("/health", (req, res) => {
res.send("App working fine ๐");
});
const PORT = process.env.PORT || 3000;
connectDB();
app.listen(PORT, () => {
console.log("Listening on port, ", PORT);
});
Above we get our app running by defining a listening port, setting up cors
and express.json()
for parsing json objects. We also created a get request at /health
which we can run at http://localhost:3000
to check if our app is running fine, before doing that run the command below in your terminal to get the app started.
nodemon app.js
After running this command, you should get a Listening on port, 3000
message on your terminal.
Setting up the App Database
We need to create a connection to our MongoDB database in order to interact with it, we will be connecting to a local MongoDB server on our system.
Create a folder db
in the root directory and add the file index.js
in it. Add the following to the index.js
file
const mongoose = require("mongoose");
require("dotenv").config()
const connectDB = () => {
mongoose.connect(process.env.MONGODB_URI);
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 = { connectDB };
Above we load a MongoDB URI from the .env
file and create a connection on it. To create your MongoDB connection uri, create a .env
in the root directory and add this environmental variable to it:
MONGODB_URI="mongodb://127.0.0.1:27017/blog"
After re-saving your index.js file, you should get a Connected to MongoDB Successfully
message on your terminal.
Defining our Application Models
We need to create MongoDB data models with mongoose to help define a structure to which we will be accepting data to it.
To get started, create a models
folder in the root directory and add the following files blog.js
and user.js
in it. Add the following lines of code to the respective files
blog.js
const mongoose = require("mongoose");
const blogSchema = mongoose.Schema({
title: {
type: String,
required: true,
unique: true,
},
description: {
type: String,
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
state: {
type: String,
enum: ["draft", "published"],
},
read_count: {
type: Number,
},
reading_time: {
type: Number,
},
tags: [{ type: String }],
body: {
type: String,
required: true,
},
created_at: { type: Date, required: true, default: Date.now },
});
const BlogModel = mongoose.model("Blog", blogSchema);
module.exports = BlogModel;
Above, we use mongoose to initialize a schema which we use to define our models. We define the various fields we will need for our blog data model. You can read more on mongoose models here. The author field which might look particulary different because it contains a ref
field is defined that way so that we can save and get the author data without having to pass the whole author data object, instead we will pass only an id and when we want to get the author information, we do that through population in mongoose. You can read more on mongoose population here.
user.js
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const userSchema = mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
min: [6, "Password too short"],
},
first_name: {
type: String,
},
last_name: {
type: String,
},
});
userSchema.pre("save", async function (next) {
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
});
userSchema.methods.isValidPassword = async function (password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
};
const UserModel = mongoose.model("User", userSchema);
module.exports = UserModel;
Above, after defining the models, we create two utility functions userSchema.pre
and userSchema.methods.isValidPassword
. The first function helps us hash / encrypt the user's password before adding it to the database while the second function will be called to help us validate the user's password when logging in. These models defined will be used in our application's controller which we will be defining below.
Creating our Application Controllers
Controllers are functions which hold our application's logic, here will be connecting to our database models that we defined earlier and sending data to our database through it.
Step One
To get started, create a new folder controllers
in the root directory and the the following files blog.controller.js
and user.controller.js
. In the user.controller.js
file, we are going to define our user sign-up and log-in logic. Add the following line of code to your user.controller.js
file:
user.controller.js
const UserModel = require("../model/user");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
dotenv.config();
const sign_user_up = async (req, res) => {
const { email, password } = req.body;
if (!email || !password || !first_name || !last_name) {
return res.status(400).json({ error: "Email and Password Required" });
}
if (password.length < 6) {
return res.status(400).json({ error: "Password too short" });
}
let user;
try {
const IsEMailRegistered = await UserModel.findOne({ email: email });
console.log(IsEMailRegistered);
if (IsEMailRegistered) {
return res.status(400).json({ error: "Email Registered" });
}
user = await UserModel.create(req.body);
} catch (err) {
return res.status(500).json({ error: err });
}
return res
.status(201)
.json({ sucess: true, message: "user created sucessfully", user: user });
};
const login_user = async (req, res) => {
const { email, password } = req.body;
if (!email || !password)
return res
.status(400)
.json({ message: "Username and password are required." });
if (password.length < 6) {
return res.status(400).json({ error: "Password too short" });
}
try {
const user = await UserModel.findOne({ email: email });
if (!user) {
return res.status(400).json({ error: "Email or Password Required" });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return res.status(400).json({ error: "Email or Password Required" });
}
const payload = {
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
id: user._id,
};
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
return res.status(200).json({ sucess: true, token: token });
} catch (e) {
return res.status(500).json({ error: e });
}
};
module.exports = { sign_user_up, login_user };
In the
sign_user_up
function we are going to define the logic for user sign-up, we start by validating the inputs fields we expect from the client and returning an error if they are empty. We also check if a user has been signed up before by connecting to our database modelUserModel.findOne
and checking if the passed email has been sent before. If that is so, we will send an error. We finally send the data passed from the clientreq.body
to our database through the model using theUserModel.create
function and return a success message with ourres
parameter.In the
login_user
function we are going to define the user log-in logic. We start by validating the inputs for emptyness. We also check for password length, we made sure it musn't be less than 6. After that, we check if user trying to login in is in our database records through the email using our modelUserModel.findOne
. If the user isn't found, we return an error. Next, we use the utility functionuser.isValidPassword
defined earlier in ouruser.js
model file to validate if the user password is correct. After that we use the user information as a payload to sign a jwt token in order to use it for user authorization. In order to sign a jwt token, we need a jwt secret which we are going to create in our.env
file. Add this to your.env
file:JWT_SECRET = "mysecret"
.Note: as a best practice, your jwt secret should never be easy to guess.
After successfully signing our jwt token, we return a success message and the token to the client.
Step two
In our blog.controller.js
file, we are going to define the logic for our blogs.
Add the following lines of code:
blog.controller.js
const moment = require("moment/moment");
const BlogModel = require("../model/blog");
const { calculateReadingTime } = require("../utils");
const getPublishedBlogs = async (req, res) => {
const {
page = 0,
perpage = 20,
tag,
author,
title,
created_at,
sortby = "created_at",
sort = "asc",
} = req.query;
const findQueryObj = {};
const sortQueryObj = {};
const sortAttributes = sortby.split(" ");
for (const attribute of sortAttributes) {
if (sort === "asc" && sortby) {
sortQueryObj[attribute] = 1;
} else if (sort === "desc" && sortby) {
sortQueryObj[attribute] = -1;
}
}
if (created_at) {
findQueryObj.created_at = {
$gt: moment(created_at).startOf("day").toDate(),
$lt: moment(created_at).endOf("day").toDate(),
};
}
if (author) {
findQueryObj.author = author;
}
if (tag) {
findQueryObj.tag = { $in: tag.split(" ") };
}
if (title) {
findQueryObj.title = title;
}
findQueryObj.state = "published";
try {
const blogs = await BlogModel.find(findQueryObj)
.populate("author")
.limit(perpage)
.skip(perpage * page)
.sort(sortQueryObj);
return res.status(200).json({ success: true, data: blogs });
} catch (e) {
return res.status(500).send("Something went wrong");
}
};
const getAPublishedBlog = async (req, res) => {
const { id } = req.params;
const blog = await BlogModel.findOne({
state: "published",
_id: id,
}).populate("author");
await BlogModel.updateOne({ _id: id }, { read_count: blog.read_count + 1 });
return res.status(200).json({ success: true, data: blog });
};
const createBlog = async (req, res) => {
const body = req.body;
try {
const blog = await BlogModel.create({
title: body.title,
description: body.description,
author: req.id,
state: "draft",
read_count: 0,
reading_time: calculateReadingTime(body.body.length),
tags: body.tags,
body: body.body,
});
return res.status(201).json({ success: true, data: blog });
} catch (err) {}
};
const publishBlog = async (req, res) => {
const { id } = req.params;
try {
const update = await BlogModel.updateOne(
{ _id: id },
{ state: "published" }
);
return res.status(200).json({ success: true, data: update });
} catch (err) {}
};
const deleteBlog = async (req, res) => {
const { id } = req.params;
try {
await BlogModel.deleteOne({ _id: id });
return res.status(200).json({ success: true, message: "Blog deleted" });
} catch (err) {
return res
.status(500)
.json({ error: err, message: "Something went wrong" });
}
};
const getUserBlogs = async (req, res) => {
const query = req.query;
const perpage = query.perpage || 10;
const page = Math.max(0, query.page || 0);
let blogs;
try {
if (query.state) {
blogs = await BlogModel.find({ state: query.state })
.populate({
path: "author",
_id: req.id,
})
.limit(perpage)
.skip(perpage * page);
} else {
blogs = await BlogModel.find({})
.populate({
path: "author",
_id: req.id,
})
.limit(perpage)
.skip(perpage * page);
}
return res.status(200).json({ success: true, data: blogs });
} catch (err) {
return res
.status(500)
.json({ error: err, message: "Something went wrong" });
}
};
module.exports = {
createBlog,
deleteBlog,
getAPublishedBlog,
getPublishedBlogs,
getUserBlogs,
publishBlog,
};
In the
getPublishedBlogs
function, we return all published blog for users to read, Note that users that are not logged in will be allowed to access this. The following queries:page = 0, perpage = 20, tag, author, title, created_at, sortby = "created_at", sort = "asc"
can be specified from the client to return a specific information needed from the published blogsIn the
getAPublishedBlog
function, we will return only a specific published blog. In order to know which blog to return, we will accept a query parameter from the client which is an id of the blog we want to returnIn the
createBlog
function, we create a blog by passing the required fields and informations, when a blog is created with our blog model, it will be saved as a draft so it wont be returned with published blogs.The
publishBlog
function publishes a blog provided the id passed as a query parameter. This allows the blog to the readable to every user.The
getUserBlogs
function allows us get the blogs of a specific user/author. We use population to get the blogs of an author, only the author's id was needed. We can get both the author's published blogs and drafts.The
deleteBlog
function deletes a blog post with a specific id passed from the client as a query parameter.
Setting Up the Application middlewares
We are going to set up two types of middleware for our applications, when a request is sent to our application through routes, we will use these middleware functions to intercept them and make the necessary validations or authorizations to decide whether to accept or decline. The two type of middleware we will be setting up are
Schema Validation Middleware with JOI
jwt authorization middleware
Schema Validation Middleware with JOI
We will need to validate the body of the request sent to the API of our blog app. I am going to demonstrate how to go about this.
To get started, create a folder in the root directory named validators
and add the file blog.js
in it. Proceed to add the following lines of code.
validators/blog.js
const Joi = require("joi");
const CreateBlogSchema = Joi.object({
title: Joi.string().required(),
description: Joi.string().required(),
author: Joi.string().required(),
tags: Joi.array().items(Joi.string()).required(),
body: Joi.string().required(),
});
async function CreateBlogValidationMW(req, res, next) {
const blogpayload = req.body;
try {
await CreateBlogSchema.validateAsync(blogpayload);
next();
} catch (error) {
next({
message: error.details[0].message,
status: 400,
});
}
}
module.exports = {
CreateBlogValidationMW,
};
Above we validate the fields that will be passed as a body to the the api request sent to createBlog
controller. We will using this alongside our application routes later.
jwt authorization middleware
Here will will validate the jwt token sent alongside the API requests for routes that require it. To get started, create a authenticate.js
in the root directory and add the following:
authenticate.js
const jwt = require("jsonwebtoken");
require("dotenv").config()
const { TokenExpiredError } = jwt;
const catchTokenExpiredError = (err, res) => {
if (err instanceof TokenExpiredError) {
return res
.status(401)
.send({ message: "Unauthorized ! Access Token was Expired" });
}
return res.status(401).send({ message: "Unauthorized!" });
};
const protect = (req, res, next) => {
const authHeader = req.headers.authorization || req.headers.Authorization;
if (!authHeader?.startsWith("Bearer "))
return res.status(401).send({ message: "Unauthorized" });
const token = authHeader.split(" ")[1];
if (!token) {
return res.status(403).send({
message: "No token provides!",
});
}
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return catchTokenExpiredError(err, res);
}
req.id = decoded.id;
next();
});
};
module.exports = protect;
We will only be accepting bearer tokens, we also handled token expiration and if a token is invalid we will reject it.
Setting up Routes for our Application
Routes are endpoints in which requests will be made to our application, let's start creating them.
To get started, create the folder routes
in the root directory and add the following files blog.routes.js
and user.routes.js
. Add the following lines of code to them.
blog.routes.js
const express = require("express");
const protect = require("../authenticate");
const {
getUserBlogs,
createBlog,
getPublishedBlogs,
publishBlog,
deleteBlog,
} = require("../controller/blog.controller.js");
const { CreateBlogValidationMW } = require("../validators/blog");
const blogRouter = express.Router();
blogRouter.get("/me", protect, getUserBlogs);
blogRouter.post("/create", protect, CreateBlogValidationMW, createBlog);
blogRouter.get("/", getPublishedBlogs);
blogRouter
.patch("/:id", protect, publishBlog)
.get("/:id", getPublishedBlogs)
.delete("/:id", protect, deleteBlog);
module.exports = blogRouter;
Above, we import and make use of the middleware we created earlier. We protected and validated the necessary routes.
user.routes.js
const express = require("express");
const { sign_user_up, login_user } = require("../controller/user.controller.js");
const userRouter = express.Router();
userRouter.post("/signup", sign_user_up);
userRouter.post("/login", login_user);
module.exports = userRouter;
In order for our routes to work properly, we must use them in the app.js
file, update the app.js
file with the marked lines of code
const express = require("express");
const authRoutes = require("./routes/user.routes.js"); // new line here!
const blogRoutes = require("./routes/blog.routes.js"); // new line here!
const cors = require("cors");
require("dotenv").config();
const app = express();
app.use(cors());
app.use(express.json());
app.use("/auth", authRoutes); // new line here!
app.use("/blog", blogRoutes); // new line here!
app.get("/health", (req, res) => {
res.send("App working fine ๐");
});
const PORT = process.env.PORT || 3000;
connectDB();
app.listen(PORT, () => {
console.log("Listening on port, ", PORT);
})
Testing our Application
Now that we are done building our blog API, we have to test it, we are going to do that with postman.
Test the following endpoints and make sure your application is running on your terminal.
User Routes
sign user up
log in user
A jwt response will be returned, make sure you copy and save it
Blog Routes
In order to use most of the blog routes we need to add a bearer token to them, to do that click on the authorization tab on postman and check the bearer token type as illustrated below. Add the token gotten from the log in request below
create blogs
publish blog
get a published blog
get author's blogs
get all published blogs
delete blog
Conclusion
That's a wrap, i hope this article was free flowing and easy to understand. You can check this github repo which contains this project files.