In Part One of this blog series, we've successfully set up our development environment, connected a MongoDB database, and started up our server in development mode.
In this part, we have the following goals:
To Create models
To Create controllers
To Create routers and define endpoints
The source code to this tutorial can be found here
To Create Models
In Part One, we defined our folder structure. Here, we created a directory called model and created a userModel.js file in it.
We will define our userModel in this file.
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;
const UserSchema = new Schema(
{
id: ObjectId,
first_name: {
type: String,
lowercase: true,
required: [true, "can't be blank"],
},
last_name: {
type: String,
lowercase: true,
required: [true, "can't be blank"],
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
// Implementing the one to many mongoose relationship.
blogPost: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "posts",
},
],
},
{ timestamps: true }
);
UserSchema.set("toJSON", {
transform: (document, returnedObject) => {
returnedObject.id = returnedObject._id.toString();
delete returnedObject._id;
delete returnedObject._v;
//To make sure the hashed password does not get to be revealed.
delete returnedObject.password;
},
});
UserSchema.pre("save", async function (next) {
const user = this;
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
if (user.isModified("password")) {
const hash = await bcrypt.hash(user.password, 10);
user.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("Users", UserSchema);
module.exports = userModel;
The code in the UserScheme.pre() function is called a pre-hook. Before the user information is saved in the database, this function will be called, you will get the plain text password, hash it, and store it to DB
We will define our postModel in this file below:
const mongoose = require('mongoose')
const slugify = require('slugify')
const Schema = mongoose.Schema
const postModel = new Schema({
title:{
type: String,
required: true,
unique: true
},
description:{
type: String,
required: false
},
author:{
type: String,
required: true,
},
//Implementing the one to many mongoose relationship.
owner:{
type: Schema.Types.ObjectId,
ref: "users"
},
body:{
type:String,
required: true
},
state:{
type: String,
enum:['draft', 'published'],
default: 'draft'
},
readCount:{
type: Number,
default:0
},
readTime:{
type: Number
},
tags: [String],
}, {timestamps: true})
postModel.pre('validate', function(next){
if(this.title){
this.slug = slugify(this.title,{lower:true,strict:true })
}
next()
})
module.exports = mongoose.model('Posts', postModel)
The 'postModel.pre' function was implemented to validate the title and then the slugify attribute or feature makes the title available to be used as a slug in the endpoints.
In the above, we imported the ODM mongoose which we installed in the previous part. Mongoose will help us communicate with our MongoDB database easily.
Next, we defined our userSchema and postSchema. This is like the skeleton or backbone of a user and the post to be made respectively. A user must have a username, email, password, and the rest of the fields seen in the schema above likewise the posts to be made by such user. Some of the fields were defined with a required attribute set to true. This means that at the point of creating a user, those required fields must be supplied. We also set a lowercase attribute to true. This will ensure that all data sent to the database are in lowercase. and enforce uniformity. Notice that we didn't add the lowercase attribute to the password field. This is because we do not want to temper with the user's password. The passwords will be saved exactly as they are entered then the hooks added to our model makes sure the password isn't just saved in plain text but hashed. We also applied the one-to-many functionality pointing one model to the other. Read up one-to-many relationships in mongoDB.
Finally, we exported our userModel and postModel so that we can access them in other files.
To Create Controllers
In the controller directory, create a userController.js file and populate it with the following:
const passport = require('passport')
const localStrategy = require('passport-local').Strategy;
const userModel = require('../model/userModel')
const JWTStrategy = require('passport-jwt').Strategy
const ExtractJWT = require('passport-jwt').ExtractJwt
// Configuring Passport Strategy.
passport.use( new JWTStrategy({
secretOrKey: process.env.JWT_SECRET_KEY,
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
},
async (token, done) =>{
try{
return done(null, token.user)
} catch(error){
done(error)
}
}
))
// With this passport middleware we can save the information provided by the user to the datebase and then pass the user information to the next middleware if succesful or throw an error.
passport.use(
'signup',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true,
},
async (req, email, password, done) => {
try {
const first_name = req.body.first_name
const last_name = req.body.last_name
const user = await userModel.create({ first_name, last_name, email, password });
return done(null, user);
} catch (error) {
done(error);
}
}
)
);
//This passport middleware authenticates the user baased on the username and password provided in the initial passport middleware.
//I.e if the user is found to have created acct, it sends the user information to the next middleware, else it throws an error.
passport.use('login', new localStrategy({
usernameField:'email',
passwordField:'password'
},
async (email, password, done) => {
try{
const user = await userModel.findOne({email})
console.log(user);
if(!user)
return done(null, false,{message: 'User not found'})
const validate = await user.isValidPassword(password)
if(!validate){
return done(null, false, {message: 'Wrong Password'})
}
return done(null, user, {message: 'Logged in succesfully'})
}catch(error){
return done(error)
}
}))
//Implementing the one-to-many mongoose relationship.
userController = {
//Helps us find the posts by author, title and tags.
find: async (req, res)=> {
const found1 = await userModel.find({author: req.params.author})
res.json(found1)
},
find: async (req, res)=> {
const found2 = await userModel.find({title: req.params.title})
res.json(found2)
},
find: async (req, res)=> {
const found3 = await userModel.find({tags: req.params.tags})
res.json(found3)
},
all: async (req, res)=> {
const allUser = await userModel.find()
res.json(allUser)
},
create: async( req, res) => {
const newUser = userModel.create(req.body)
const savedUser = await newUser.save()
res.json(savedUser)
},
getAllPosts: async (req, res)=> {
const foundUser1 = await userModel.find({author: req.params.author}).populate("blogPosts")
res.json(foundUser1)
const foundUser2 = await userModel.find({title: req.params.title}).populate("blogPosts")
res.json(foundUser2)
const foundUser3 = await userModel.find({tags: req.params.tags}).populate("blogPosts")
res.json(foundUser3)
}
}
module.exports = userController
Still in the controller directory, create a postController.js file and populate it with the following:
N/B: Notice that some of the requirements listed in the Part One of this blog series are implemented in this controller files.
const express = require("express");
const moment = require("moment");
const postModel = require("../model/postModel");
const postRouter = require("../routes/postRoute");
const userModel = require('../model/userModel')
const { query } = require("express");
//Add Blog Post CRUD Controller Functions.
const createBlogPost = async (req, res, next) => {
try {
const user = await userModel.findById(req.user._id)
console.log("user =>", user);
if(user){
// Algorithm for calculating read_time.
function read_time() {
const postTexts = req.body.body;
//Average adult reading word per minute(wpm) is 225.
const wpm = 225;
//Hence
const words = postTexts.trim().split(/\s+/).length;
return Math.ceil(words / wpm)
}
const readTime = read_time();
console.log("readTime =>",readTime);
const blogPostCreated = {
title: req.body.title,
tags: req.body.tags,
author: `${user.first_name} ${user.last_name}`,
owner: user._id,
description: req.body.description,
body: req.body.body,
readTime,
};
blogPostCreated.lastUpdateAt = new Date() // set the lastUpdateAt to the current date
const blogPost = await postModel.create(blogPostCreated);
return res.status(201).json({ status: true, blogPost });
} else( res.status(401).json({message: "user not found"}))
} catch (err) {
next(err);
}
};
const getBlogPost = async (req, res, next) => {
try {
const blogPost = await postModel.findById(req.params.blogPostId)
if (!blogPost) {
res.status(404).json({ status: false, blogPost: null });
return
}
blogPost.readCount++
await blogPost.save()
res.status(200).json({ status: true, blogPost })
} catch (err) {
next(err);
}
};
const getAllBlogPost = async (req, res, next) => {
try {
const query = req.query;
const {
created_at,
state = "published",
order_by = ("read_count", "reading_time", "created_at"),
page = 1,
per_page = 20,
} = query;
//Query object
const findQuery = {};
//Used moment npm package to implement our timestamp.
if (created_at) {
findQuery.created_at = {
$gt: moment(created_at).startOf("day").toDate(),
$lt: moment(created_at).endOf("day").toDate(),
};
}
//Searchable by
const sortQuery = {};
const sortAttributes = order_by.split(",");
const blogPost = await postModel
.find(findQuery)
.sort(sortQuery)
.skip(page)
.limit(per_page);
return res.status(200).json({ status: true, blogPost });
} catch (err) {
next(err);
}
}
;
const updateBlogPost = async (req, res)=>{
const { id } = req.params
let blogPost = await postModel.findById(id)
console.log("blogPost =>", blogPost)
if(!blogPost){
return res.send(404).json({ status: false,message: "blogPost not found" })
}
blogPost.lastUpdateAt = new Date() // set the lastUpdateAt to the current date
await postModel.findOneAndUpdate({_id: id}, {state: req.body.state})
blogPost = await postModel.findById(id)
return res.json({status: true, blogPost})
}
const deleteBlogPost = async (req, res) =>{
const { id } = req.params
console.log("ID =>", id);
const blogPost = await postModel.deleteOne({_id : id})
if(!blogPost){
return res.status(401).json({status: false, blogPost:null, message: "Error deleting wrong post"})
}
return res.status(201).json({status: true, blogPost})
}
module.exports = { createBlogPost,getBlogPost, getAllBlogPost, updateBlogPost, deleteBlogPost}
In the above, we defined and exported the userController file. This file will contain all of our functions. Our first function will create a user, and this was done with aid of passport npm package using jwt-strategy. In creating a new user, we extract token from the header as bearer token, while creating our secret key variable stored in the dotenv file. This jwt strategy was used to set up the 'signup route' by accepting two parameters in the usernameField and passwordField of the client(in my case I used thunder client) requiring user to input email, and password for signup. These might come from the input boxes at the point of registration. Also, remember that we set these parameters to have a required attribute in our model. So if any of the three parameters isn’t supplied, our code won’t run. An error message will be shown, telling us that one or more parameters are missing.
Next, we run a check against the database to know if a user with a supplied email or username already exists. This is because we set the unique status on the username and email attributes to true. So if another user wants to register with either an already registered username or email, it won’t work. If the username and email address do not exist already, we proceed to hash the password using Bcrypt’s hash method. This takes in two arguments: our plain text password and the number of level of encryption we’d want to use on our password. In this case, we used 10.
Then we use the same jwt strategy to authenticate the login route.We did this by using the findOne query to search for the signed up user's email as an object then validate it with respect to what we have in the database. This is done with the 'isValidPassword' function,which is also a hook we added to the database model (if you remember😁). The user attempting to login has to provide the matching email and password used for signup. When this is validated it gives a message 'logged in successfully'.
Don't get tired yet, stay with me.🤗
Implementing the one-to-many mongoose relationship.
Because one of our requirements which is infact a standard for building blogs, requires that a user be able to find posts by author, titlte, and tags or description as the case may be. The relationship describes a relationship where one component of your project can have more than one relationship to other components or features of the project. An example is a Blog where a blog might have many Comments but a Comment is only related to a single Blog. So to implement this, we must know that the data coming into the relational database must come as an array.
Recall: In the userModel file we have this.
// Implementing the one to many mongoose relationship.
blogPost: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "posts",
},
],
Next create a userController function and populate it witht the following block of codes.
//Implementing the one-to-many mongoose relationship.
userController = {
//Helps us find the posts by author, title and tags.
find: async (req, res)=> {
const found1 = await userModel.find({author: req.params.author})
res.json(found1)
},
find: async (req, res)=> {
const found2 = await userModel.find({title: req.params.title})
res.json(found2)
},
find: async (req, res)=> {
const found3 = await userModel.find({tags: req.params.tags})
res.json(found3)
},
all: async (req, res)=> {
const allUser = await userModel.find()
res.json(allUser)
},
create: async( req, res) => {
const newUser = userModel.create(req.body)
const savedUser = await newUser.save()
res.json(savedUser)
},
getAllPosts: async (req, res) => {
const foundUser1 = await userModel.find({author: req.params.author}).populate("blogPosts")
res.json(foundUser1)
const foundUser2 = await userModel.find({title: req.params.title}).populate("blogPosts")
res.json(foundUser2)
const foundUser3 = await userModel.find({tags: req.params.tags}).populate("blogPosts")
res.json(foundUser3)
}
}
module.exports = userController
We create variables to store the find query we're making on the database, these variables we named them found1, found2, and found3 respectively. A params is used to find the title,tag and author from the incoming request. To find all users in the database we use the allUser variable to make the qeury to the database. Lookin at the code above what I did was to pass in the variables holding the find query I intend to make and then returning them as json. From the 'getAllPosts' function the blogPosts array we created in the one-to-many relationship were populated with respect to the search type(wether by author, tags or title).
Next we look at the postController functions while I try to explain the functions in the file.
The createBlogPost function takes in the user variable with which we first check by 'findById' to find and make sure the user exists in the database. If the user exists we then perform some other requirements for our blogging API e.g readcount algorithm, after which we use an object in a 'createdBlogPost' function to take in the parameters from the request body for creating the blogpost e.g title,owner,description and body. Then with a variable 'blogPost' we call the create method that creates and saves a blogpost to the database.If there's an error, it is taken care of in the catch block.
The getBlogPost function helps to find a particular post, and this is done using the postID which is gotten from the params-ID attribute of the particular post in question. So we use the 'findById' and pass in a blogpostID as data. We now apply the increment functionality and the save to database then send a 200 success code while the catch block takes care of any errors.
The getAllBlogPost function helps to find and fetch all posts created, this posts as we stated in the requiremets fro our blog can be sorted by some conditions like created_at, state = "published", order_by = ("read_count", "reading_time", "created_at"), then make the page =1, per_page = 20, then fetches all the blogposts created and saved in the database.
The updateBlogPost is implemented by using the findById method to get a particular postID from the request params then check and confirm if it's the blogpost, if it's not it will throw an error else it will use the findOneAndUpdate method to update the particular postID provided.
The deleteBlogPost is implemented by simply destructuring the ID variable from the request params and then calling the deleteOne method on the database. Next we run an if statement to make sure it's the blogpost with that particular ID we want to delete, if it's not it an error will throw up else we'd get a 200 success message.
Then we export all the controller functions to be usable in another file.
To Create router and define endpoints
In this section, we'll define our routes and HTTP request methods. This will be in our routes directory. Create a postRoute.js and userRoute.js file in the routes directory and paste the following code in it:
For the userRoute.js file we use jwt.
const express = require('express')
const passport = require('passport')
const JWTStrategy = require('passport-jwt').Strategy
const ExtractJWT = require('passport-jwt').ExtractJwt
const jwt = require('jsonwebtoken')
const dotenv = require('dotenv').config()
const userRouter = express.Router()
userRouter.post('/signup', passport.authenticate('signup', { session: false }), async (req, res, next) =>{
res.json({
message: "SignUp successful",
user: req.user
})
})
userRouter.post('/login', async (req, res, next)=>{
passport.authenticate('login', async (err, user, info)=> {
try{
if(err){
return next(err)
}
if(!user){
const error = new Error('Username or password is incorrect')
return next(error)
}
req.login(user, {session: false},
async (error)=> {
if (error) return next(error)
const body = { _id: user._id, email: user.email}
//ADD EXPIRATION TIME, ONCE EXCEEDED, REFRESH TOKEN IS REQUIRED, AND USER IS LOGGED OUT
// OR THE USER NEEDS TO LOGIN AGAIN
const token = jwt.sign({ user: body }, process.env.JWT_SECRET_KEY, { expiresIn: '1h' });
return res.json({token})
}
)
} catch(error){
return next(error)
}
}
) (req, res, next)
})
module.exports = userRouter
Jason web token(jwt) and passport was used in the file above to authenticate and the jwt specifically was used to sign the routes for the signup and login fuctionalities we implemented in the controller functions. We generated a secret key for the jwt signing and then made the expiration to last for 1hr once exceeded,fresh token is required, and user is logged out or the user needs to login again. The check block catches the error if any. Then we export the userRouter function.
Create the postRoute.js file in the route folder and populate it with the following code.
const express = require("express");
const passport = require('passport')
const jwt = require('jsonwebtoken')
const postController = require('../controllers/postController')
const blogValidation = require('../middlewares/validator')
const postRouter = express.Router();
postRouter.post('/',passport.authenticate('jwt', { session: false }), blogValidation, postController.createBlogPost)
//Unauthenticated route, to get one/all published blogs by logged in and non-logged-in users.
postRouter.get('/:blogPostId',postController.getBlogPost)
postRouter.get('/',postController.getAllBlogPost)
postRouter.patch('/edit/:id',passport.authenticate('jwt', { session: false }) ,postController.updateBlogPost)
postRouter.delete('/delete/:id', passport.authenticate('jwt', { session: false }),postController.deleteBlogPost)
module.exports = postRouter;
In the above, we imported and initialized postRouter from the express package installed earlier. Next, we imported our postController. We destructured for readability. Using the router we initialized, we define 4 HTTP request methods: GET, POST, PATCH, and DELETE. These methods will take in the appropriate functions and execute them as required. We exported the router so we can call it in server.js.
Let’s update index.js as follows:
const express = require("express");
const { connectToMongoDB } = require("./db");
const passport = require('passport')
const rateLimit = require('express-rate-limit');
const winston = require('winston')
const logger = require('./logger/logger')
const httpLogger = require('./logger/httpLogger')
const userController = require('./controllers/userController')
const userRoute = require('./routes/userRoute')
const postRoute = require("./routes/postRoute");
const { urlencoded } = require("express");
const { getAllBlogPost } = require("./controllers/postController");
const app = express();
const PORT = 5000;
//connecting to MonGoDB Instance
connectToMongoDB();
// Defaults to in-memory store.
// You can use redis or any other store.
const limiter = rateLimit({
windowMs: 0.5 * 60 * 1000, // 15 minutes
max: 4, // 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
});
// Apply the rate limiting middleware to all requests
app.use(limiter);
app.use(express.urlencoded({ extended: false }));
//Registeration Middleware Routes
app.use('/',userRoute);
app.use('/signup', passport.authenticate('jwt', { session: false }),userRoute);
app.use('/login', passport.authenticate('jwt', { session: false }),userRoute);
app.use('/posts',postRoute);
//Home route
app.get('/', (req, res) => {
res.send('Welcome to the blog API');
});
//Users Routes
app.get("/users",userController.all );
app.get("/users/create", userController.create);
app.get("/users/:author", userController.find);
app.get("/users/:author/posts",userController.getAllPosts);
app.get('/posts')
// 404 route
app.use('*', (req, res) => {
return res.status(404).json({ message: 'route not found' })
})
// Start server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}....!`);
});
In the above, we imported the router and called it in index.js.
It’s time to test what we’ve been building and correct errors if any. Start your server by running the following command:
npm start
This will start your server in development mode. Open your postman or thunderclient and test all your endpoints.
To create a user, add localhost:5000/signup to the URL bar. Change the HTTP request method to POST and add your user details as depicted in the image below. If you followed everything accordingly, you should have a success message printed out as shown below. Checking your database, you’ll see the user you just created.
Let’s login with our signed up user. Change the URL to localhost:5000/login. Note that the token generated has to be copied. This token is generated in your postman/thunderclient as an object. Also, change the HTTP method to POST. You should have something similar to the image below after clicking on send.
The token we have generated for authorization is then added to the auth session of the thunderclient to authenticate that this user can create a blog post,we now set the HTTP method to post so as to create a blogpost as shown below.
Next is the image showing where our post was being created. See below
To get a particular blog post in the database, we use the id shown in the ID property of any of the blogpost then paste it in the url of the http request. This will show us this:
If you followed the process meticulously everyother endpoint should work very well. To avoid make this blog series lengthier than it should(I understand it already is😅), I'd let you try out the other endpoints yourself, while we quickly look out some extra features from the middleware folder.
Adding Logger and Validation to our Project.
As the standard is, there's need for every project to have a way of saving the logs made to the app, so as to allow the developer trace calls made to the application. To implement this we use a package called winston. This npm package gives us access to utilize the logger feature. So we create a logger folder having a httpLogger.js and a logger.js file inside it, in this logger.js file we require winston as follows:
For the httpLogger.js file
const morgan = require('morgan')
const json = require('morgan-json')
const logger = require('./logger')
const format = json({
method: ':method',
url: ':url',
status: ':status',
contentLength: ':res[content-length]',
responseTime: ':response-time'
})
const httpLogger = morgan(format, {
stream: {
write: (message) => {
const {
method,
url,
status,
contentLength,
responseTime
} = JSON.parse(message)
logger.info('HTTP Access Log', {
timestamp: new Date().toString(),
method,
url,
status: Number(status),
contentLength,
responseTime: Number(responseTime)
})
}
}
})
module.exports = httpLogger
For the logger.js file we have;
const winston = require('winston')
const options = {
file: {
level: 'info',
filename: './logs/app.log',
handleExceptions: true,
json: true,
maxsize: 5242880, // 5MB
maxFiles: 5,
colorize: false,
},
console: {
level: 'debug',
handleExceptions: true,
json: false,
colorize: true,
},
};
const logger = winston.createLogger({
levels: winston.config.npm.levels,
transports: [
new winston.transports.File(options.file),
new winston.transports.Console(options.console)
],
exitOnError: false
})
module.exports = logger
With the setup we have above, each time we fire up our server and make a call to an endpoin we get the app.logs folder automatically created and a file insde this folder showing the activities and time for such activities carried on our application as shown below;
Next is to add validations to our application. This validations place a form of regulation on what can be done or can't be done within our application e.g the length of the characters of the title of our blogpost, or the minimun price of a product, e.t.c. We implement this using 'joi', an npm package. A middleware folder is created then it a file validator.js is created and we populate it with the code below;
const joi = require('joi');
const validateBlogMiddleWare = (req, res, next) => {
const blogPayload = req.body;
const { error } = blogValidator.validate(blogPayload);
if (error) {
console.log(error);
return res.status(406).send(error.details[0].message);
}
next();
};
const blogValidator = joi.object({
title: joi.string().min(5).max(255).required(),
// shortDescription: joi.string().min(5).max(255).optional(),
// year: joi.number().min(1900).max(2023).required(),
// isbn: joi.string().min(10).max(13).required(),
// price: joi.number().min(0).required(),
createAt: joi.date().default(Date.now()),
lastUpdateAt: joi.date().default(Date.now()),
});
module.exports = validateBlogMiddleWare;
The blogMiddleware we created and exported above is then added to the postRoute.js file inside the routes folder aa seen below;
const express = require("express");
const passport = require('passport')
const jwt = require('jsonwebtoken')
const postController = require('../controllers/postController')
const blogValidation = require('../middlewares/validator')
const postRouter = express.Router();
postRouter.post('/',passport.authenticate('jwt', { session: false }), blogValidation, postController.createBlogPost)
//Unauthenticated route, to get one/all published blogs by logged in and non-logged-in users.
postRouter.get('/:blogPostId',postController.getBlogPost)
postRouter.get('/',postController.getAllBlogPost)
postRouter.patch('/edit/:id',passport.authenticate('jwt', { session: false }) ,postController.updateBlogPost)
postRouter.delete('/delete/:id', passport.authenticate('jwt', { session: false }),postController.deleteBlogPost)
It serves as a middleware inside the particular route where it's added; which is in the post route for blog post creation.
This project can now be deployed on cyclic.
Congratulations!👍 You have successfully created a BlogAPI with CRUD utilities.