Introduction
In this article, we are going to create a blog API using Node.js. A Blog API in which users can signup, create, read, update and delete blogs, and even filter blogs with various parameters.
Technology Stack
Back-end: Node.js, Express
Database: MongoDB
Prerequisites
Basic knowledge of JavaScript, and MongoDB
Node.js and npm installed on your machine
MongoDB server installed and running on your machine
Implementation
Below is the step-by-step implementation of the above approach.
Create a Project Folder
It is good practice to have all files and folders properly organized on the system. For this blog project, we will create a dedicated folder on the computer. We can do this with the computer's file manager, VS Code or the terminal. Here's the terminal script to create a new directory (folder) and then move into it;
$ mkdir blogApp
$ cd blogApp
Initialize NPM
Locate your project folder into the terminal & type the command
npm init -y
It initializes our node application & makes a package.json file.
Install Dependencies
Locate your root project directory and open terminal, these packages are required to get the project started, we’d install more packages as needed.
npm install express nodemon mongoose morgan
Structure of the project
We are gonna adopt an MVC structure for this project, MVC stands for model view controllers. The model folder would contain all the database schema and database logic, controllers would contain all the request and response handlers, and it could also contain functions that are meant to manipulate data
Install dotenv package and add a .env file. This file would contain all our environment variables and secret keys etc
npm i dotenv
.env
PORT = 3030
Let's test that our server is working by creating our app.js file. This would serve as our entry point
app.js
const express = require('express');
const PORT = process.env.PORT || 3000;
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.end('Home Page');
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
to start up server run
nodemon app.js
Setup database
Add our mongodb uri to the .env file check here for details on how to get connection uri
Create a db folder and add a connection.js file
connection.js
const moogoose = require('mongoose');
require('dotenv').config();
const MONGODB_URI = process.env.MONGODB_URI;
// connect to mongodb
function connectDB() {
moogoose.connect(MONGODB_URI);
moogoose.connection.on('connected', () => {
console.log('Connected to MongoDB successfully');
});
moogoose.connection.on('error', (err) => {
console.log('Error connecting to MongoDB', err);
})
}
module.exports = { connectDB };
Update the app.js to connect
const express = require('express');
const PORT = process.env.PORT || 3000;
const connectDB = require("./db/connection");
const app = express();
connectDB()
app.get('/', (req, res) => {
res.end('Home Page');
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
Models
We have set up our db now let us set up our models for the app. We need to set up a user model and a blog model. Add user.js and blog.js to models folder
user.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const bcrypt = require("bcrypt");
const userSchema = new Schema({
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
passWord: {
type: String,
required: true,
},
blogs: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Blog",
},
],
});
userSchema.set("toJSON", {
transform: (document, returnedObject) => {
delete returnedObject.__v;
delete returnedObject.passWord;
},
});
// Encrypt password before saving to the database
userSchema.pre("save", async function (next) {
const user = this;
if (!user.isModified("passWord")) return next();
const hash = await bcrypt.hash(
this.passWord,
10
);
this.passWord = hash;
next();
});
userSchema.methods.comparePasswords =
async function (password) {
return await bcrypt.compare(
password,
this.passWord
);
};
const User = mongoose.model("User", userSchema);
module.exports = User;
As observed we added a 3rd party library to hash our passwords before saving to db. Type the following code in the terminal
npm i bcrypt
blog.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const readingTime = require("../utils/readingAlgo");
const blogSchema = new Schema(
{
title: {
type: String,
required: true,
unique: true,
},
description: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
state: {
type: String,
default: "draft",
enum: ["draft", "published"],
},
readCount: {
type: Number,
default: 0,
},
readingTime: Number,
tags: [String],
body: {
required: true,
type: String,
},
},
{ timestamps: true }
);
const Blog = mongoose.model("Blog", blogSchema);
module.exports = Blog;
Validation
Next step we need to add validations to check user input and handle wrong user input with appropriate error messages. We would be using joi for validation
npm i joi
Create a validations folder and add validation.js
validation.js
const joi = require("joi");
const validateUserSignup = async (
req,
res,
next
) => {
const userPayload = req.body;
try {
await userSignupValidator.validateAsync(
userPayload
);
next();
} catch (error) {
console.log(error);
return res
.status(406)
.send(error.details[0].message);
}
};
const validateUserLogin = async (
req,
res,
next
) => {
const userPayload = req.body;
try {
await userLoginValidator.validateAsync(
userPayload
);
next();
} catch (error) {
console.log(error);
return res
.status(406)
.send(error.details[0].message);
}
};
const validateBlogMiddleWare = async (
req,
res,
next
) => {
const blogPayload = req.body;
try {
await blogValidator.validateAsync(
blogPayload
);
next();
} catch (error) {
console.log(error);
return res
.status(406)
.send(error.details[0].message);
}
};
const blogValidator = joi.object({
title: joi.string().min(5).max(255).required(),
description: joi
.string()
.min(5)
.max(255)
.optional(),
// author: joi.objectId().required(),
body: joi.string().required(),
state: joi.string().default("draft"),
readCount: joi.number().default(0),
tags: joi.string().optional(),
});
const userSignupValidator = joi.object({
firstName: joi.string().required(),
lastName: joi.string().required(),
email: joi.string().required(),
passWord: joi
.string()
.min(5)
.max(255)
.required(),
});
const userLoginValidator = joi.object({
email: joi.string().required(),
passWord: joi
.string()
.min(5)
.max(255)
.required(),
});
module.exports = {
validateBlogMiddleWare,
validateUserSignup,
validateUserLogin,
};
Controllers
Next , we need to set up controllers to handle requests and give out responses. For our users, we need to set up a strategy that handles login and sign up
We would use passport and JWT for authentication
npm i passport-jwt passport-local passport jsonwebtoken
Create a file passport.js
passport.js
const passport = require("passport");
const localStrategy =
require("passport-local").Strategy;
const User = require("../model/users.model");
const JWTstrategy =
require("passport-jwt").Strategy;
const ExtractJWT =
require("passport-jwt").ExtractJwt;
require("dotenv").config();
passport.use(
"signup",
new localStrategy(
{
usernameField: "email",
passwordField: "passWord",
//ALLOWS REW PARAMS TO BE PASSED
passReqToCallback: true,
},
async (req, email, passWord, done) => {
try {
const { firstName, lastName } = req.body;
const user = await User.create({
firstName,
lastName,
email,
passWord,
});
return done(null, user);
} catch (error) {
done(error);
}
}
)
);
// ...
passport.use(
"login",
new localStrategy(
{
usernameField: "email",
passwordField: "passWord",
},
async (email, passWord, done) => {
try {
const user = await User.findOne({
email,
});
if (!user) {
return done(null, false, {
message: "User not found",
});
}
const validate =
await user.comparePasswords(passWord);
if (!validate) {
return done(null, false, {
message: "Wrong Password",
});
}
return done(null, user, {
message: "Logged in Successfully",
});
} catch (error) {
done(error);
}
}
)
);
passport.use(
new JWTstrategy(
{
secretOrKey: process.env.JWT_SECRET,
jwtFromRequest:
ExtractJWT.fromAuthHeaderAsBearerToken(),
},
async (token, done) => {
try {
const user = await User.findById(
token.user
);
if (user) {
return done(null, user);
}
return done(null, false);
} catch (e) {
next(err);
}
}
)
);
To make use of jwt we need to add a secret key to the .env file
.env
JWT_SECRET = {add your secret key}
Add a user.controller.js file to the controller folder
user.js
const User = require("../model/users.model");
const passport = require("passport");
const jwt = require("jsonwebtoken");
const signup = async (req, res, next) => {
res.json({
message: "Signup successful",
user: req.user,
});
};
const login = async (req, res, next) => {
passport.authenticate(
"login",
async (err, user, info) => {
try {
if (err || !user) {
const error = new Error(
"Wrong email/password"
);
return next(error);
}
req.login(
user,
{ session: false },
async (error) => {
if (error) return next(error);
const body = {
_id: user._id,
email: user.email,
};
const token = jwt.sign(
{ user: body },
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
return res.json({
message: "login successful",
token,
});
}
);
} catch (error) {
console.log(error);
next(error);
}
}
)(req, res, next);
};
module.exports = {
signup,
login,
};
User routes
We need to mount our routes and setup endpoints Add a user.routes.js
user.routes.js
const userController = require("../controllers/users.controller");
const passport = require("passport");
const {
signup,
login,
} = require("../controllers/users.controller");
require("../authentication/passport");
const {
validateUserSignup,
validateUserLogin,
} = require("../validations/validator");
const userRouter = require("express").Router();
userRouter.get("/users", userController.getUsers);
userRouter.post(
"/signup",
validateUserSignup,
passport.authenticate("signup", {
session: false,
}),
signup
);
userRouter.post(
"/login",
validateUserLogin,
login
);
module.exports = userRouter;
Update app.js
app.js
const express = require('express');
const PORT = process.env.PORT || 3000;
const userRouter = require("./routes/user.routes");
const app = express();
app.use(express.json());
app.use("/", userRouter);
app.get('/', (req, res) => {
res.end('Home Page');
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
After updating our app.js file, let’s now start test our user endpoints using postman or thunder client.
Blog Controllers
Next we create our blog controllers to handle different requests as follows
[if !supportLists]1. [endif]Create a new post
[if !supportLists]2. [endif]Get all posts
[if !supportLists]3. [endif]Get single post
[if !supportLists]4. [endif]Get all posts belonging to a logged in user
[if !supportLists]5. [endif]Get single post belonging to a logged in user
[if !supportLists]6. [endif]Update the post body only if it’s the owner of the post making the request
[if !supportLists]7. [endif]Update state of a post only if it’s the owner of the post making the request
[if !supportLists]8. [endif]Delete a post only if it’s the owner of the post making the request
Create a blogs.controller.js in controller folder
blog.controller.js
const Blogs = require("../model/blog.model");
const Users = require("../model/users.model");
const mongoose = require("mongoose");
const getAllBlogs = async (req, res) => {
const queryParam = {
state: "published",
};
const {
page = 1,
limit = 20,
sortBy,
author,
title,
tags,
} = req.query;
const { orderBy } = req.query || "asc";
const p = page || 1;
let blogsPerPage = (p - 1) * limit;
const sort = {};
if (sortBy && orderBy) {
sort[sortBy] = orderBy === "asc" ? 1 : -1;
}
if (author) queryParam.author = author;
if (title) queryParam.title = title;
try {
const blogs = await Blogs.find(queryParam)
.select("title description author -_id")
.limit(limit)
.skip(blogsPerPage)
.sort(sort);
if (blogs.length === 0) {
res.status(404);
res.send("Blog Not Found");
return;
}
res.json({ status: true, blogs });
} catch (err) {
console.log(err);
return res.status(400);
}
};
//LOGGED IN AND NON LOGGED IN USERS GET PUBLISHED BLOG
const getBlog = async (req, res) => {
const { query } = req;
const { title, id } = query;
const queryParam = {};
if (title) queryParam.title = title;
if (id) queryParam._id = id;
try {
const blog = await Blogs.findOne(
queryParam
).populate({
path: "author",
select: "firstName lastName -_id",
});
if (!blog) {
res.status(404).send({
status: false,
message: "Blog not found",
});
return;
}
if (blog.state !== "published") {
res.status(404);
res.json({
status: false,
message: "Blog not found",
});
return;
}
blog.readCount = blog.readCount + 1;
await blog.save();
res.status(200);
res.json({ status: true, blog });
return;
} catch (err) {
res.status(400);
res.json({
status: false,
});
return;
}
};
const getBlogById = async (req, res, next) => {
const { id } = req.params;
try {
const blog = await Blogs.findOneAndUpdate(
{ _id: id, state: "published" },
{ $inc: { readCount: 1 } },
{ new: true }
).populate({
path: "author",
select: "-email -blogs",
});
if (!blog) {
res.status(404);
res.json({
status: false,
message: "Blog not found",
});
return;
}
res.status(200);
res.json(blog);
return;
} catch (err) {
next(err);
}
};
const newBlog = async (req, res) => {
const body = req.body;
const author = req.user.id;
body.author = author;
const wordCount = body.body.split(" ").length;
body.readingTime = ((wordCount) => {
return Math.round((wordCount / 200) * 60);
})(wordCount);
try {
const user = await Users.findById(author);
const blog = await Blogs.create(body);
user.blogs = user.blogs.concat(blog._id);
await user.save();
res.status(201);
res.json({ status: true, blog });
} catch (err) {
res.status(400);
res.send("Bad request");
}
};
const editBlog = async (req, res) => {
const { id } = req.params;
const { title, description, body, tags } =
req.body;
await Blogs.findByIdAndUpdate(
{ _id: id },
{
title,
description,
body,
$push: { tags: tags },
},
{ new: true }
);
return res.status(201).json({
message: "Blog successfully updated",
});
};
const updateBlog = async (req, res) => {
const { id } = req.params;
try {
const blog = await Blogs.findOneAndUpdate(
{
_id: id,
state: "draft",
},
{ $set: { state: "published" } },
{ new: true }
).select("state title description -_id");
if (!blog) {
res.status(400);
res.send("Blog published already");
return;
}
res.status(200).json({
message: "Blog state successfully updated",
blog,
});
} catch (err) {
next();
}
};
const deleteBlog = async (req, res) => {
const { id } = req.params;
try {
Blogs.deleteOne({ _id: id }, function (err) {
if (err) return res.send(err);
res.status(200);
res.json({
status: true,
message: "Deleted",
});
});
return;
} catch (error) {
next();
}
};
const getMyBlogs = async (req, res) => {
const author = req.user.id;
const _id = mongoose.Types.ObjectId(author);
const {
state,
page = 1,
limit = 20,
sortBy,
orderBy,
} = req.query;
const p = page;
let blogsPerPage = (p - 1) * limit;
const sort = {};
if (sortBy && orderBy) {
sort[sortBy] = orderBy === "asc" ? 1 : -1;
}
let blog;
try {
if (state) {
blog = await Blogs.find({
author: _id,
state: state,
})
.select("title description state")
.limit(limit)
.skip(blogsPerPage)
.sort(sort);
} else {
blog = await Blogs.find({
author: _id,
})
.select("title description state")
.limit(limit)
.skip(blogsPerPage)
.sort(sort);
}
if (blog.length != 0) {
res.status(200);
res.json({
status: true,
blog: blog,
});
} else {
res.status(404).send("No Blogs created");
return;
}
} catch (error) {
next(error);
}
};
const getMyBlogById = async (req, res) => {
const { id } = req.params;
try {
const blog = await Blogs.findOneAndUpdate(
{ _id: id },
{ $inc: { readCount: 1 } },
{ new: true }
);
if (!blog) {
res.status(404);
res.json({ status: false });
return;
}
res.status(200);
res.json({ status: true, blog });
return;
} catch (err) {
next(err);
}
};
module.exports = {
updateBlog,
deleteBlog,
getMyBlogs,
getMyBlogById,
getBlog,
getAllBlogs,
editBlog,
newBlog,
getBlogById,
};
We need to add an algorithm that calculates reading time and adds it to database before saving hence we add the following code to our blog model
// calculate reading time before saving document
blogSchema.pre("save", function (next) {
let blog = this;
// do nothing if the article body is unchanged
if (!blog.isModified("body")) return next();
// calculate the time in minutes
const readTime = readingTime(this.body);
blog.readingTime = readTime;
next();
});
Blog Routes
Next we need to create our endpoints that receives the requests
Add a file called blog.routes.js in the routes folder
blog.routes.js
const blogRouter = require("express").Router();
require("../authentication/passport");
const passport = require("passport");
const authorize = require("../authentication/authorize");
const blogController = require("../controllers/blogs.controllers");
const {
validateBlogMiddleWare,
} = require("../validations/validator");
blogRouter
.route("/blogs")
.get(blogController.getAllBlogs);
blogRouter
.route("/blog")
.get(blogController.getBlog);
blogRouter
.route("/getblog/:id")
.get(blogController.getBlogById);
blogRouter.use(
"/",
passport.authenticate("jwt", {
session: false,
})
);
blogRouter
.route("/blogs")
.post(
validateBlogMiddleWare,
blogController.newBlog
);
blogRouter
.route("/myblogs")
.get(blogController.getMyBlogs);
blogRouter
.route("/myblogs/:id")
.get(blogController.getMyBlogById)
.put(blogController.editBlog)
.patch(blogController.updateBlog)
.delete(blogController.deleteBlog);
module.exports = blogRouter;
We need to mount to be able to test our blog app
Update app.js
app.js
const blogRouter = require("./routes/blog.routes");
app.use("/api", blogRouter);
After updating our app.js file, let’s now test our blog endpoints.
Authorization
Next we add authorization to allow only authorize users access certain endpoints and make certain requests
Create a folder named authentication and add file authorize.js
authorize.js
const Blogs = require("../model/blog.model");
const authorize = async (req, res, next) => {
const author = req.user.id;
const { id } = req.params;
try {
const blog = await Blogs.findOne({ _id: id });
if (!blog) {
res.status(404).send({
message: "Blog not found",
status: false,
});
return;
}
const blogId = blog.author.valueOf();
if (blogId === author) {
next();
} else {
throw new Error("auth");
}
} catch (err) {
next(err);
}
};
module.exports = authorize;
This file serves as a middleware to check if authenticated and authorized users are trying to access a protected route
To add update the blog.routes.js file
const blogRouter = require("express").Router();
require("../authentication/passport");
const passport = require("passport");
const authorize = require("../authentication/authorize");
const blogController = require("../controllers/blogs.controllers");
const {
validateBlogMiddleWare,
} = require("../validations/validator");
blogRouter
.route("/blogs")
.get(blogController.getAllBlogs);
blogRouter
.route("/blog")
.get(blogController.getBlog);
blogRouter
.route("/getblog/:id")
.get(blogController.getBlogById);
blogRouter.use(
"/",
passport.authenticate("jwt", {
session: false,
})
);
blogRouter
.route("/blogs")
.post(
validateBlogMiddleWare,
blogController.newBlog
);
blogRouter
.route("/myblogs")
.get(blogController.getMyBlogs);
blogRouter.use("/myblogs/:id", authorize);
blogRouter
.route("/myblogs/:id")
.get(blogController.getMyBlogById)
.put(blogController.editBlog)
.patch(blogController.updateBlog)
.delete(blogController.deleteBlog);
module.exports = blogRouter;
Error Handling
Express handles errors that occur synchronously we need to add custom error handlers to handle asynchronous errors
Create a folder middlewares and add errorHandler.js
errorHandler.js
const errorHandler = (err, req, res, next) => {
if (err.message === "auth") {
res.status(401);
res.send("Unauthorized");
return;
} else if (err.type === "input") {
res.status(400);
return res.send("invalid input");
} else if (
err.message.startsWith(
"Cast to ObjectId failed"
)
) {
res.status(400);
return res.send("Invalid Blog ID");
} else if (
err.message.startsWith(
"E11000 duplicate key error collection"
)
) {
res.status(400).json({
message: "email already in use",
});
} else if (
err.message.startsWith(
"User validation failed"
)
) {
res.status(400).json({
message: "Invalid/Incomplete details",
});
} else {
res.status(500);
return res.send("Internal error");
}
};
const invalidPathHandler = (req, res, next) => {
res.status(400).send({
message: `${req.originalUrl} is not a valid path`,
});
next();
};
module.exports = {
errorHandler,
invalidPathHandler,
};
Now we add the error handler middleware after all others so that all errors are passed down with the next() function until it gets to the handler
Update app.js with the following code
const express = require('express');
const PORT = process.env.PORT || 3000;
const userRouter = require("./routes/user.routes");
const blogRouter = require("./routes/blog.routes");
const {
errorHandler,
invalidPathHandler,
} = require("./middlewares/errorHandlers");
const app = express();
app.use(express.json());
app.use("/", userRouter);
app.use("/api", blogRouter);
app.get('/', (req, res) => {
res.end('Home Page');
});
app.all("*", invalidPathHandler);
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
API Security
We can add some third party libraries to add security to our api
npm i express-rate-limit helmet
Rate limiting is a technique to limit network traffic to prevent users from exhausting system resources. Rate limiting makes it harder for malicious actors to overburden the system and cause attacks like Denial of Service (DoS) and Helmet helps you secure your Express apps by setting various HTTP headers.
Update the app.js
const express = require('express');
const PORT = process.env.PORT || 3000;
const userRouter = require("./routes/user.routes");
const blogRouter = require("./routes/blog.routes");
const {
errorHandler,
invalidPathHandler,
} = require("./middlewares/errorHandlers");
const rateLimit = require("express-rate-limit");
const helmet = require("helmet");
const limiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the RateLimit-* headers
legacyHeaders: false, // Disable the X-RateLimit-* headers
});
//add secuirty
app.use(helmet());
app.use(express.json());
app.use(morgan("dev"));
// Apply the rate limiting middleware to API calls only
app.use("/api", limiter);
app.use("/", userRouter);
app.use("/api", blogRouter);
app.all("*", invalidPathHandler);
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
Deployment
Push codes to github and deploy to a hosting service such as Heroku, cyclic or render. Set up the appropriate environment variables for the hosting services. Always make sure to add .env file that contains sensitive information to .gitignore to prevent it from being uploaded to github
Wrap Up
We have come to the end of this tutorial, and we have been able to build a Node.js Blog API, where Users can Sign up, Create and publish their blog posts/articles, update, edit and delete their articles also we touched on input validations, authentication, authorization, error handling and API security.
I hope you learnt a lot from this tutorial. All codes used can be found at GitHub