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 actions

  • The 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.