How to build a blog api with Nodejs, ExpressJs and Mongodb.
my project blog api with altschool africa
Table of contents
INTRODUCTION
I am part of Altschool Africa software engineering backed student for 2021; as a second-semester project we were asked to build a blog api from start to finish with what we have learnt in Mongodb, nodejs expressjs & how to deploy the api.
Let's go through the structure of the api so we could understand what we are building.
Blog API structure:
# API STRUCTURE
* GET / (unprotected) ==> HOME Route to display articles home content
* GET / LOgin (unprotected) ==> Authenticate a new User
* GET / Logout (unprotected) ==> LOgout a new User
## Articles API ROUTES
* GET /articles (protected) => Return all artic;les
* POST /articless (protected) => Add articles to the DB
* PUT /articles/:id (protected) => Update articles
* DELETE /articles/:id (protected) => Delete article by ID
* GET /articles/:id (protected) => Get articles by unique ID
The requirements for building this api are as follow:
# REQUIREMENTS
* User should be able to signup
* User should be able to login with Passport using JWT
* User(s) should be able to get articles
* User should be able to create articles
* User should be able to update and delete articles
* Test application
The dependencies installed to actualize what we want to build:
# SETUP
* install dependecies:
* bcrypt
* dotenv
* express
* jest
* jsonwebtoken
* mongoose
* passport
* passport-jwt
* passport-local
* passport-local-mongoose
* supertest
Use this command to install the dependencies
$ npm i name-of-dependenc(ies)y
Before you install the dependencies initialize your express app with this command.
okay yes with every prompt till it initializes.
$ npm init -y
The structure of the api folder and files:
The Controller folder houses three files:
ArticleController: contains the article design
AuthController: contains the authentication for login and logout with Passport-JWT and password protection with bcrypt
GetArticles: contain the route controller to get articles that are not protected.
The database folder houses just one file.
index.js: it contains the database configuration connection to Mongodb with mongoose.
The models folder houses three files:
article.js : contains the structure of the article and how it can be modelled in the database.
user.js: contains the structure of the users and how it can be modelled in the database.
index.js: exports the models.
The routes folder contains three files:
ArticlesRoutes.js: contains all the routes to the article
authRoutes.js: contains the authentication routes for login and logout.
The test and utils folders houses one file each.
home.test.js: contains testing for my homepage
utils.js: contains the implementation of a feature ReadingTime; give the exact time a customer reads the article
We have the next four important files:
.env: contains all the environmet variables that enables the connection and configuration of the api to the database server and web server.
.gitignore: Is used to hide the environment variables in the .env file.
app.js: contains the middleware, routes, and imports of all necessary modules that makes up the MVC( Model-View-Controller).
server.js: contains the configuration of the app server so it can run on the web.
Let's go into how to implement the api features into these structured folders and files.
# controller folder
//articleController.js
const {ArticleModel} = require('../models')
exports.createArticle = ('/', async (req, res, next) => {
const newArticleCreated = {
title: req.body.title,
desc: req.body.desc,
author: req.user._id,
tags: req.body.tags,
body: req.body.body,
}
const newArticle = await ArticleModel.create(newArticleCreated);
res.status(201).json({status: 'successful',
data: {
ArticleModel: newArticle,
},
})
});
exports.getMyArticle = async(req, res) =>{
const id = req.params.id
const info = req.body
const userInfo = req.user
const IdArticle = await ArticleModel.find({_id: id})
IdArticle.read_Count++
res.json({author: IdArticle.author, read_Count: IdArticle.read_Count, article: IdArticle})
}
exports.deleteArticle = async (req, res) => {
const id = req.params.id
const info = req.body
const userInfo = req.user
let IdArticle = await ArticleModel.findByIdAndDelete(id, info)
IdArticle = await ArticleModel.findById(id)
res.status(200).json({status: 'successful' })
}
exports.updateArticle = async (req, res) => {
const { id } = req.params;
const { state } = req.body;
const article = await ArticleModel.findById(id)
if (!article) {
return res.status(404).json({ status: false, article: null })
}
console.log(article)
if (state == article.state) {
return res.status(422).json({ status: false, article: null, message: 'Invalid operation' })
}
article.state = state;
await article.save()
return res.json({ status: true, article })
}
//AuthController
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const {UserModel} = require('../models');
require("dotenv").config();
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
passport.use(
new JWTstrategy(
{
secretOrKey: process.env.JWT_SECRET,
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken() // Use this if you are using Bearer token
},
async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}
)
);
// This middleware saves the information provided by the user to the database,
// and then sends the user information to the next middleware if successful.
// Otherwise, it reports an error.
passport.use(
'signup',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true,
},
async (req, email, password, done) => {
try {
const firstname = req.body.firstname
const lastname = req.body.lastname
const user = await UserModel.create({firstname, lastname, email, password });
return done(null, user);
} catch (error) {
done(error);
}
}
)
);
// This middleware authenticates the user based on the email and password provided.
// If the user is found, it sends the user information to the next middleware.
// Otherwise, it reports 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);
console.log(validate)
console.log(user)
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}
)
);
//GetArticles
const { ArticleModel } = require("../models");
exports.getArticles = ('/', (req, res) => {
ArticleModel.find({})
.then(title => {
res.status(200).json(title)
})
.catch(err => {
console.log(err)
res.send(err)
});
});
This will allow a user to create an account and login and logout successfully and get articles even before login in.
# Database folder
//index.js
const mongoose = require('mongoose');
require('dotenv').config();
const MONGODB_URL = process.env.MONGODB_URL
function connect() {
mongoose.connect(MONGODB_URL)
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 = {
connect
};
connects to our database; Mongodb through Mongoose.
#models folder
//article.js
const mongoose = require('mongoose');
const { readingTime } = require('../utils/utils')
const ArticleModel = new mongoose.Schema({
title: {
type: String,
required: true,
unique: true
},
desc : String,
author: {
type: String,
required: true,
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
state: {
type: String,
default: 'draft', enum: ['draft', 'published']
},
read_Count: {
type: Number,
default: 0
},
readingTime: Number,
tags: {
type: [String ],
default: [],
}
})
// calculate reading time before saving document
ArticleModel.pre('save', function (next) {
let article = this
// do nothing if the article body is unchanged
if (!article.isModified('body')) return next()
// calculate the time in minutes
const timeToRead = readingTime(this.body)
article.readingTime = timeToRead
next()
})
ArticleModel.set('toJSON', {
transform: (document, returnedObject) => {
delete returnedObject.__v
},
})
module.exports = mongoose.model('Article', ArticleModel);
//users.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const article = require('./article');
const Schema = mongoose.Schema;
const UserModel = new Schema({
firstname: { type: String,
required: true
},
lastname: { type: String,
required: true
},
email: { type: String,
required: true,
unique: true
},
password: { type: String,
required: true
},
article: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Article',
}
],
})
UserModel.set('toJSON', {
transform: (_document, returnedObject) => {
returnedObject.id = returnedObject._id.toString()
delete returnedObject._id
delete returnedObject.__v
// the passwordHash should not be revealed
delete returnedObject.password
},
});
// 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.
// UserModel.pre(
// 'save',
// async function (next) {
// const user = this;
// const hash = await bcrypt.hash(this.password, 10);
// this.password = hash;
// // if the password is modified do something
// if (user.isModified('password'))
// { const hash =await bcrypt.hash(user.password, 10)
// user.password = hash
// }
// next();
// }
// );
// // You will also need to make sure that the user trying to log in has the correct credentials. Add the following new method:
// UserModel.methods.isValidPassword = async function(password) {
// const user = this;
// const compare = await bcrypt.compare(password, user.password);
// return compare;
// }
UserModel.pre('save', function(next) {
const user = this;
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
// hash the password using our new salt
bcrypt.hash(user.password, 10, function(err, hash) {
if (err) return next(err);
// override the cleartext password with the hashed one
user.password = hash;
next();
});
});
UserModel.methods.isValidPassword = function(password){
const passwordHash = this.password
return new Promise((resolve,reject) => {
bcrypt.compare(password,passwordHash,(err,same)=>{
if(err){
return reject(err)
}
resolve(same)
})
})
}
const User = mongoose.model('User', UserModel);
module.exports = User;
We just encrypted the user password with hashing and user model.
//index.js
const ArticleModel = require('./article')
const UserModel = require('./user')
module.exports = {
ArticleModel,
UserModel,
}
#Routes folder
//articlesRoutes
const express = require('express')
const ArticleController = require('../controllers/ArticleController');
const getarticles = require("../controllers/getArticles")
const articleRouter = express.Router();
articleRouter.post('/', ArticleController.createArticle)
articleRouter.get('/:id', ArticleController.getMyArticle)
articleRouter.patch('/:id', ArticleController.updateArticle)
articleRouter.delete('/:id', ArticleController.deleteArticle)
articleRouter.get('/', getarticles.getArticles)
module.exports = articleRouter;
//AuthRoutes
const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const authRouter = express.Router();
authRouter.post(
'/signup',
passport.authenticate('signup', { session: false }), async (req, res, next) => {
res.json({
message: 'Signup successful',
user: req.user
});
}
);
authRouter.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('email 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 };
//You store the id and email in the payload of the JWT.
// You then sign the token with a secret or key (JWT_SECRET), and send back the token to the user.
// DO NOT STORE PASSWORDS IN THE JWT!
const token = jwt.sign({ user: body }, process.env.JWT_SECRET);
return res.json({ token });
}
);
} catch (error) {
return next(error);
}
}
)(req, res, next);
}
);
module.exports = authRouter;
Just configured the routes so that they can be used in the app.js when exported there.
#Test folder
//home.test.js
const request = require('supertest')
const app = require('../app');
describe('Home Route', () => {
it('Should return status true', async () => {
const response = await request(app).get('/').set('content-type', 'application/json')
expect(response.status).toBe(200)
expect(response.body).toEqual({ status: true })
})
it('Should return error when routed to undefined route', async () => {
const response = await request(app).get('/undefined').set('content-type', 'application/json')
expect(response.status).toBe(404)
expect(response.body).toEqual({ message: 'route not found' })
})
});
Tested the home route
to run the test use this command.
$ npm test
#utils folder
//utils.js
const readingTime = (article) => {
const noOfWords = article.split(' ').length
// assuming the average person reads 200 words a minute
const wordsPerMinute = noOfWords / 200
return Math.round(wordsPerMinute) === 0 ? 1 : Math.round(wordsPerMinute)
}
module.exports = { readingTime }
Implemented ReadingTime for the articles when read.
.env
//.env file
PORT = 4000
MONGODB_URL =mongodb+srv://ayibatonye-ikemike:1993200214Tonye@cluster0.zfb1abr.mongodb.net/blog_api
JWT_SECRET = 'jnjbhbhbhjbcdhdhdhdhdjddjdcfnfnmnvmvmvmvmvmvmvmvmdddd'
To connect to Mongodb we have to create a free account with Mongodb Atlas
through this link: https://www.mongodb.com/atlas/database
After that create a database collection for articles and users.
After creating a cluster click on connect and get the mongodb_url
Input your user password after copying and pasting it at the .env file.
app.js
//app.js
const passport = require('passport');
const bodyParser = require('body-parser');
const ArticleRouter = require('./routes/articlesRoutes');
const authRouter = require('./routes/authRoutes');
const express = require("express");
require("dotenv").config();
const app = express()
//middleware
require("./controllers/AuthController") // Signup and login authentication middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.json());
// routes
app.use('/', authRouter);
app.use('/article', passport.authenticate('jwt', { session: false }), ArticleRouter)
app.use('/getarticles', ArticleRouter )
// home route
app.get('/', (req, res) => {
return res.json({ status: true })
})
// 404 route
app.use('*', (req, res) => {
return res.status(404).json({ message: 'route not found' })
})
module.exports = app;
The express app is configured.
server.js
//server.js
const database = require("./database/index");
const app = require("./app")
require('dotenv').config();
const PORT = process.env.PORT
// connect to database
database.connect();
//start server
app.listen(PORT, () => {
console.log('Listening on port, ', PORT)
})
Now we can run the server with this command.
$ node server.js
Test your routes with postman.
After you finish testing your routes you can commit your blog_api to git and github.
through this link: https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/adding-locally-hosted-code-to-github
Then deploy your blog api with render from a github repository
through this link: https://www.freecodecamp.org/news/how-to-deploy-nodejs-application-with-render/
CONCLUSION
We just finished building an api with nodejs, expressjs, and MongoDB and deployed it through render.