Elogs Blog
Introduction
I was tasked with creating a blogging API. There were certain requirements which included
A user should able to sign up and sign in to the blog app
The owner of the blog should be logged in to perform actionsThe MVC pattern should be used
JWT should be used as an authentication strategy and expire the token after 1 hour
Prerequisites
Basic understanding of JavaScript, Node.js and MongoDB
Node.js, npm and MongoDB installed.
Setting up
In your terminal, create a new directory for the project and navigate to it.
open up the directory in VScode by entering the command 'code .'
In VScode open a new terminal
Initialize a new Node.js project by running 'npm init'
Ensure the following dependencies are installed
express
MongoDB
mongoose
bcrypt
jsonwebtoken
$ mkdir directoryName
$ cd directoryName
$ code .
VScode terminal
$ npm init -y
$ npm install mysql
$ npm install bcrypt
$ npm install jsonwebtoken
Creation of a dotenv file (.env file)
A .env file is used to organize and maintain your environment variables. To use the file you need to first install it by running 'npm install dotenv'
Creation of DataBase Connection
Next, a file that will be used to connect to the MongoDB database and test the connection was created. the file 'db.js' was created and the code below added to set it up.
const mongoose = require('mongoose');
require("dotenv").config();
const MDB_URL = process.env.MDB_URL
function connectToMongoDB() {
mongoose.connect(MDB_URL)
mongoose.connection.on('connected', ()=>{
console.log("Connected to Mongodb successfully")
})
}
mongoose.connection.on('error', (err)=>{
console.log(err)
console.log('Error connecting to Mongodb')
})
module.exports = {connectToMongoDB }
App entry point (index.js)
The code here imports the already installed Express.js framework. it also imports the database file, the dotenv file, the routes, middleware and the utils. This code creates an express app and sets up middleware and routes for handling requests. enable CORS for all requests and sets up an error-handling middleware. It will also start the app and tells it to listen for requests on the port indicated in the dotenv file. A breakdown of some parts of thecode is available below
The first ten lines import various modules that are needed for the app. Express is a web framework for Node.js. signup routes and blog routes are custom modules that define routes for the app, Errorhandler is a middleware that catches errors that occur both synchronously and asynchronously.
Create blog and user models
The code below is used to create the blog and user models. the b log and user models represent the structure of the data, th eformat and constraints with which it is stored
Blog model
const mongoose = require ('mongoose')
const Schema = mongoose.Schema
const BlogModel = new Schema({
title: {
type: String,
unique: true,
required: true,
},
description: {
type: String},
author: {
type : String,
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required:true
},
state: {
type: String,
default: 'draft',
enum: ['draft', 'published'],
},
read_count: {
type: Number,
default: 0,
},
reading_time: Number,
tags: [String],
body: String,
},
{ timestamps: true }
)
module.exports = mongoose.model('Blog',BlogModel)
User model
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const UserModel = new mongoose.Schema({
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
username: {
type: String,
required: true,
unique: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
articles: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Article',
},
],
})
UserSchema.pre('save', function (next) {
let user = this
if (!user.isModified('password')) return next()
bcrypt.hash(user.password, 10, (err, hash) => {
if (err) return next(err)
user.password = hash
next()
})
})
UserSchema.methods.passwordIsValid = function (password) {
const passwordHash = this.password
return new Promise((resolve, reject) => {
bcrypt.compare(password, passwordHash, (err, same) => {
if (err) {
return reject(err)
}
resolve(same)
})
})
}
UserSchema.set('toJSON', {
transform: (document, returnedObject) => {
delete returnedObject.__v
delete returnedObject.password
},
})
module.exports = mongoose.model('User', UserModel)
Route
const router = require('express').Router()
const Controller = require('../controllers/blogs')
const { filterAndSort, filterByPublished, list, setUserFilter } = require('../middleware/apiFeatures')
const { getUserFromToken, attachUser } = require('../middleware/verifyUser')
const pagination = require('../middleware/pagination')
const isCreator = require('../middleware/isCreator')
router.route('/')
.get(filterAndSort, filterByPublished, pagination, list, blogController.getBlogs)
.post(getUserFromToken, blogController.createBlog)
router.route('/p')
.get(getUserFromToken, filterAndSort, setUserFilter, pagination, blogController.getBlogs)
router.route('/:id')
.get(attachUser, blogController.getBlog)
.patch(getUserFromToken, isCreator, blogController.updateBlogState)
.put(getUserFromToken, isCreator, blogController.updateBlog)
.delete(getUserFromToken, isCreator, blogController.deleteBlog)
module.exports = router
Controler
The controller handles the user request and generates the appropriate response. The code below alows the user to sign up, sign into the blog, update blog, delete blog etc. Both the user and the login controllers require the user model while the blog controller requires the blog model.
blog controller
const Blog = require('../models/Blog')
const createBlog = async (req, res, next) => {
try {
// grab details from the request
const { title, description, tags, body } = req.body
// create blog object
const newBlog = new Blog({
title,
description: description || title,
tags,
author: req.user._id,
body,
owner: req.user.username,
})
// save to database
const createdBlog = await newBlog.save()
// save blog ID to user document
req.user.articles = req.user.articles.concat(createdBlog._id)
await req.user.save()
// return response
return res.status(201).json({
status: 'success',
data: createdBlog,
})
} catch (err) {
err.source = 'creating a blog'
next(err)
}
}
const getBlogs = async (req, res, next) => {
try {
const blogs = await Blog
.find(req.findFilter)
.sort(req.sort)
.select(req.fields)
.populate('author', { username: 1 })
.skip(req.pagination.start)
.limit(req.pagination.sizePerPage)
const pageInfo = req.pageInfo
return res.json({
status: 'success',
pageInfo,
data: blogs,
})
} catch (err) {
err.source = 'get published blogs controller'
next(err)
}
}
const getBlog = async (req, res, next) => {
try {
const { id } = req.params
const blog = await Blog.findById(id).populate('author', { username: 1 })
if (!blog) {
return res.status(404).json({
status: 'fail',
message: 'Blog not found'
})
}
if (blog.state !== 'published') {
const response = (res) => {
return res.status(403).json({
status: 'fail',
error: 'Requested article is not published',
})
}
if (!req.user) {
return response(res)
} else if (blog.author._id.toString() !== req.user.id.toString()) {
return response(res)
}
}
// update blog read count
blog.read_count += 1
await blog.save()
return res.json({
status: 'success',
data: blog,
})
} catch (err) {
err.source = 'get published blog controller'
next(err)
}
}
const updateBlogState = async (req, res, next) => {
try {
let { state } = req.body
if (!(state && (state.toLowerCase() === 'published' || state.toLowerCase() === 'draft'))) {
throw new Error('Please provide a valid state')
}
const blog = await Blog.findByIdAndUpdate(req.params.id, { state: state.toLowerCase() }, { new: true, runValidators: true, context: 'query' })
if (!blog) {
return res.status(404).json({
status: 'fail',
message: 'Blog not found'
})
}
return res.json({
status: 'success',
data: blog
})
} catch (err) {
err.source = 'update blog'
next(err)
}
}
const updateBlog = async (req, res, next) => {
try {
let blogUpdate = { ...req.body }
if (blogUpdate.state) delete blogUpdate.state
const blog = await Blog.findByIdAndUpdate(req.params.id, blogUpdate, { new: true, runValidators: true, context: 'query' })
if (!blog) {
return res.status(404).json({
status: 'fail',
message: 'Blog not found'
})
}
return res.json({
status: 'success',
data: blog
})
} catch (err) {
err.source = 'update blog'
next(err)
}
}
const deleteBlog = async (req, res, next) => {
const user = req.user
try {
const deletedBlog = await Blog.findByIdAndRemove(req.params.id)
if (!deletedBlog) {
return res.status(404).json({
status: 'fail',
error: 'Blog not found'
})
}
const deletedBlogId = deletedBlog._id
const index = user.articles.indexOf(deletedBlogId)
user.articles.splice(index, 1)
await user.save()
res.json({
status: 'success',
data: deletedBlog
})
} catch (err) {
next(err)
}
}
module.exports = {
createBlog,
getBlogs,
getBlog,
updateBlog,
updateBlogState,
deleteBlog,
}
login controller
const router = require('express').Router()
const User = require('../models/User')
const jwt = require('jsonwebtoken')
const { SECRET } = require('../config/config')
router.route('/').post(async (req, res, next) => {
try {
// grab username and password from request
const { username, password } = req.body
// check database for user
const user = await User.findOne({ username })
const passwordIsValid = user === null ? false : await user.passwordIsValid(password)
if (!(user && passwordIsValid)) {
return res.status(403).json({
message: 'Username/password is incorrect',
})
}
const userForToken = {
username: user.username,
id: user._id,
}
const validityPeriod = '1h'
const token = jwt.sign(userForToken, SECRET, { expiresIn: validityPeriod })
res.json({ token, username: user.username, name: user.firstName })
} catch (e) {
next(e)
}
})
module.exports = router
user controller
const User = require('../models/User')
const createUser = async (req, res, next) => {
try {
// grab details from the request
const { firstName, lastName, username, email, password } = req.body
// create user object
const newUser = new User({
firstName,
lastName,
username,
email,
password,
})
// save to database
const createdUser = await newUser.save()
// return response
return res.status(201).json({
status: 'success',
data: createdUser,
})
} catch (err) {
next(err)
}
}
module.exports = {
createUser,
}
Deployment
The back-end can be deployed to a hosting service such as Heroku or cyclic. Ensure the appropriate environment variables are set up.
Conclusion
We have successfully created a blogging API and deployed it
Thank you for reading.