After going through the API design and features we'd be implementing in our previous article, let us delve straight into the full implementation in codes
Setting up our Server
Before we set up our server, let's take a look at connecting to our database. In our config
folder, let's create a database.js
file and copy and paste the code below:
const mongoose = require("mongoose")
const dotenv = require("dotenv")
dotenv.config({ path: "./config/.env" })
const MONGODB_URI =
process.env.NODE_ENV === "test"
? process.env.TEST_MONGODB_URI
: process.env.MONGODB_URI
const connectDB = async () => {
await mongoose
.connect(MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then((conn) => {
console.log(
`Conected to Mongo! Database name: ${conn.connections[0].name}`
)
})
.catch((err) => console.error("Error connecting to mongo", err))
}
module.exports = connectDB
The dotenv
package allows us to read values from the .env
file we created earlier using the process.env.VARIABLE_NAME
command and mongoose
is an ORM that helps relate with the MongoDB database. The MONGODB_URI is gotten from mongodb atlas. You can check out this video for refesher. The database is exported as a module to be used in another file.
Let's link this up with our server code shown below.
const app = require("./app")
const connectDB = require("./config/database")
const http = require("http")
const path = require("path")
const dotenv = require("dotenv")
dotenv.config({ path: path.join(__dirname, "./config/.env") })
const PORT = process.env.PORT || 4040
const server = http.createServer(app)
connectDB().then(() => {
server.listen(PORT, async () => {
console.log(`Server running on port ${PORT}`)
})
})
The connectDB()
function is called and on resolving, the server is spawned. This ensures that we're connected to the database first in order to interact with it
This is a server running on our local machine and on the value of PORT
from our .env
file or 4040
. The app
in our server is the heart of all of our logic and this is where all the several pieces that makes up our blog API come together.
Having talked about setting up our server and connecting to the database, we also want to know who our database looks like, how the information collected are stored in an organised Schema
created with mongoose
Models
Two Schemas(blog.js
and users.js
) were created for this project in the models
folder in our project folder structure. One for the Blog and one for Users.
The Blog Model:
const mongoose = require("mongoose")
const Schema = mongoose.Schema
const objectId = Schema.Types.ObjectId
const BlogSchema = new Schema(
{
title: {
type: String,
required: [true, "Please enter a title"],
unique: true
},
description: {
type: String,
required: [true, "Please enter a description"]
},
owner: {
type: String
},
author: {
type: objectId,
ref: "User"
},
state: {
type: String,
default: "draft",
enum: ["draft", "published"]
},
readCount: {
type: Number
},
readingTime: {
type: Number
},
tags: [String],
body: {
type: String,
required: [true, "Please provide the blog content"]
}
},
{ timestamps: true }
)
//! Add the blog reading time before saving
BlogSchema.pre("save", function (next) {
let blog = this
let titleLength = blog.title.length
let descriptionLength = blog.description.length
let bodyLength = blog.body.length
let totalLength = titleLength + descriptionLength + bodyLength
let totalTime = Math.round(totalLength / 200)
blog.readCount = 0
blog.readingTime = totalTime == 0 ? 1 : totalTime
blog.tags = blog.tags.map((tag) => tag.toLowerCase())
next()
})
//! Delete certain fields before returning result to client
BlogSchema.set("toJSON", {
transform: (document, returnedObject) => {
delete returnedObject.__v
}
})
const Blog = mongoose.model("Blog", BlogSchema)
module.exports = Blog
The User Model:
const mongoose = require("mongoose")
const bcrypt = require("bcrypt")
const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId
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
},
repeatPassword: {
type: String,
required: true
},
userType: {
type: String,
default: "user",
enum: ["admin", "user"]
},
blogs: [
{
type: ObjectId,
ref: "Blog"
}
]
},
{ timestamps: true }
)
//! Encrypt password before saving it to database
UserSchema.pre("save", function (next) {
let user = this
if (!user.isModified("password")) return next()
bcrypt.hash(user.password, 8, (err, hash) => {
if (err) return next(err)
user.password = hash
user.repeatPassword = hash
next()
})
})
//! Compare inputted password with the password in the database
UserSchema.methods.comparePassword = function (pword) {
const passwordHash = this.password
return new Promise((resolve, reject) => {
bcrypt.compare(pword, passwordHash, (err, same) => {
if (err) {
return reject(err)
}
resolve(same)
})
})
}
//! Delete certain fields before returning result to client
UserSchema.set("toJSON", {
transform: (document, returnedObject) => {
returnedObject.id = returnedObject._id.toString()
delete returnedObject._id
delete returnedObject.__v
delete returnedObject.password
delete returnedObject.repeatPassword
}
})
const User = mongoose.model("user", UserSchema)
module.exports = User
Both models represent the model of the information we want to recieve from our users and store in our database. The User model is used for authentication
. They both also have pre hooks
that run just before the data is saved to the database. The user password is hashed
or encrypted
before it is saved to the database using the bcrypt
package. The user model has a method
that is also used to compare user's inputted password with that in the database when trying to login.
The next thing we want to talk about are the endpoints but before then, let's look at some of the middleware codes we implemented so we can get the flow of request objects through the middlewares and the eventual request handler or controller.
Let's create a middleware
folder as in our file structure and put inside the folder we create the auth.js
, filter.js
and paginate.js
. This leads us to explaining the implementation of the middleware features discussed in the previous article.
Authentication
This middleware code is contained in the auth.js
file we just created. It contains the following codes:
const passport = require("passport")
const localStrategy = require("passport-local").Strategy
const UserModel = require("../models/users")
const JWTstrategy = require("passport-jwt").Strategy
const ExtractJWT = require("passport-jwt").ExtractJwt
const path = require("path")
const dotenv = require("dotenv")
dotenv.config({ path: path.join(__dirname, "../config/.env") })
const { BadRequestError, UnauthenticatedError } = require("../errors")
const opts = {}
opts.secretOrKey = process.env.JWT_SECRET
opts.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken()
module.exports = (passport) => {
passport.use(
new JWTstrategy(opts, async (token, done) => {
try {
const user = token.user
return done(null, user)
} catch (err) {
done(err)
}
})
)
passport.use(
"signup",
new localStrategy(
{
usernameField: "email",
passwordField: "password",
passReqToCallback: true
},
async (req, email, password, done) => {
try {
const { firstName, lastName, repeatPassword } = req.body
const userObject = {
firstName,
lastName,
email,
password,
repeatPassword
}
const user = new UserModel(userObject)
const savedUser = await user.save()
return done(null, savedUser)
} catch (err) {
done(err)
}
}
)
)
passport.use(
"login",
new localStrategy(
{
usernameField: "email",
passwordField: "password"
},
async (email, password, done) => {
try {
if (!email || !password) {
throw new BadRequestError("Please provide email or password")
}
const user = await UserModel.findOne({ email })
if (!user) {
throw new UnauthenticatedError("Please provide valid credentials")
}
const validate = await user.comparePassword(password)
if (!validate) {
throw new UnauthenticatedError("Please provide valid credentials")
}
return done(null, user, { message: "Logged in Successfully" })
} catch (err) {
done(err)
}
}
)
)
}
Passport handles our authentication using the JWT strategy
(a token based authentication). It also allows us to use the local Strategy
to run bothe the sign up and login feature of the application. The JWT strategy allows for a token
to be generated after it has been verified to release a payload
containing information that can be used to access the next request handler.
Filter
This middleware code is contained in the filter.js
file we just created. It contains the following codes:
const filterByPublished = (req, res, next) => {
req.filterObject.state = "published"
return next()
}
const filterAndSort = (req, res, next) => {
const { tag, author, title, state, sort } = req.query
req.filterObject = {}
req.sortObject = {}
try {
if (tag) {
const searchTag = Array.isArray(tag)
? tag.toLowerCase()
: tag.split(", " || " " || ",").map((t) => t.toLowerCase())
req.filterObject.tags = { $in: searchTag }
}
if (author) {
req.filterObject.owner = { $regex: author, $options: "i" }
}
if (title) {
req.filterObject.title = { $regex: title, $options: "i" }
}
if (sort) {
const sortList = sort.split(",").join(" ")
req.sortObject.sort = sortList
} else {
req.sortObject.sort = "createdAt"
}
if (state) {
req.filterObject.state = state
} else {
req.filterObject.state = { $in: ["draft", "published"] }
}
return next()
} catch (err) {
next(err)
}
}
module.exports = {
filterByPublished,
filterAndSort
}
When we try to search for published blogs based on certain criteria such as authors, tags, titles, we have to add query
to our request. These queries are collected in this middleware and passed on to the next request handler in order to be used.
Paginate
This middleware code is contained in the paginate.js
file we just created. It contains the following codes:
const paginate = (req, res, next) => {
req.paginate = {}
const blogsPerPage = 20
try {
if (req.query.p) {
const page = req.query.p
const numOfBlogsToSkip = (page - 1) * blogsPerPage
req.paginate.blogsPerPage = blogsPerPage
req.paginate.numOfBlogsToSkip = numOfBlogsToSkip
return next()
}
const page = 1
let numOfBlogsToSkip = (page - 1) * blogsPerPage
req.paginate.blogsPerPage = blogsPerPage
req.paginate.numOfBlogsToSkip = numOfBlogsToSkip
return next()
} catch (err) {
next(err)
}
}
module.exports = paginate
The paginate middleware sends the number of blogs per page
and number of blogs to skip per page
to the next request handler.
Now that we are done with the middlewares that preceed our controllers, let us talk about our Routes and Controllers.
Routes
Let's not forget the summary of the endpoints in our previous article in this series. We are going to be linking the routes in our endpoints to a particular controller and/or middleware
Let's take a look
The Blog Routes
const express = require("express")
const passport = require("passport")
const {
createBlog,
getAllBlogs,
updateBlog,
updateBlogState,
getAllUsersBlogs,
getBlogById,
deleteBlog,
getBlogByIdAuth
} = require("../controllers/blog")
const { filterAndSort, filterByPublished } = require("../middlewares/filter")
const paginate = require("../middlewares/paginate")
const validateBlog = require("../validators/blog.validator")
const blogRouter = express.Router()
blogRouter
.route("/home/blog")
.get(filterAndSort, filterByPublished, paginate, getAllBlogs)
blogRouter
.route("/home/blog/:id")
.get(filterAndSort, filterByPublished, getBlogById)
blogRouter.use("/blog", passport.authenticate("jwt", { session: false }))
blogRouter
.route("/blog")
.get(filterAndSort, paginate, getAllUsersBlogs)
.post(validateBlog, createBlog)
blogRouter
.route("/blog/:id")
.get(filterAndSort, getBlogByIdAuth)
.put(validateBlog, updateBlog)
.patch(updateBlogState)
.delete(deleteBlog)
module.exports = blogRouter
The blog route makes use of the the express Router() method and we can see how it links our various routes to middlewares and controllers. It is important to note that the order of the middlewares are important and should be followed strictly. The user route follows the same pattern.
The input validator used in thr validateBlog
function is a middleware that helps validate the user input withe the help of a package known as joi. You can check the validator folder in the source code link belw for it implementation.
The Controllers
They handle all of our CRUD operations. There are quite a number of controllers in this project and I would be picking three of them to talk about. One that requires authentication, one that does not then one that requires parameter
.
The POST or create a new blog controller
const createBlog = async (req, res, next) => {
const { id } = req.user
const { title, description, body, tags } = req.body
const user = await UserModel.findById({ _id: id })
try {
const blog = new BlogModel({
title,
description,
owner: `${user.firstName} ${user.lastName}`,
body,
tags,
author: id
})
const savedBlog = await blog.save()
user.blogs = user.blogs.concat(savedBlog.id)
await user.save()
return res.status(201).json(savedBlog)
} catch (err) {
next(err)
}
}
The req.user
is the payload handed down from the authentication middleware and we can see how we used our BlogModel to create a new blog and at the same time save the reference ID of the blog to the list of blogs created by that user.
The GET all published blog controller
const getAllBlogs = async (req, res, next) => {
const filters = req.filterObject
const { sort } = req.sortObject
const { blogsPerPage, numOfBlogsToSkip } = req.paginate
try {
const blog = await BlogModel.find(filters, { title: 1, description: 1 })
.sort(sort)
.skip(numOfBlogsToSkip)
.limit(blogsPerPage)
return res.status(200).json({ count: blog.length, blogs: blog })
} catch (err) {
next(err)
}
}
This controller does not require authentication and we have the filterObject, sortObject and paginate attached to requests coming their respective middlewares. It returns just the title
and description
of all the blogs.
The GET blog published blog by ID controller
const getBlogById = async (req, res, next) => {
try {
const { state } = req.filterObject
const { id } = req.params
const blog = await BlogModel.findOneAndUpdate(
{ _id: id, state: state },
{ $inc: { readCount: 1 } },
{ new: true }
)
if (!blog) {
throw new NotFoundError(`No blog with id ${id} to update`)
}
return res.status(200).json(blog)
} catch (err) {
next(err)
}
}
Getting blog by id
requires that a parameter is added to the route in order to open that specific blog.
Everything we have discussed individually are brought together in the app.js
file which can be checked in the source code provided below.
Wrapping Up
All of the codes above were abstracted from the main source code and should not be run separately on their own. The source code should be checked to further understanding and also to be able to link how all the modules work hand in hand.
As time goes on, specific parts of the code will be spoken upon with reference to this.
Let's talk about testing and deployment in our next article.
Top comments (0)