Creating a RESTful Blog API with NodeJS, Express, MongoDB and JWT

Creating a RESTful Blog API with NodeJS, Express, MongoDB and JWT

Introduction

This is a follow-up for my second Semester Exam BLOG API project at AltSchool Africa.

You can clone the repo from here to follow up with the tutorial, and you can also view the live App here.

In this article, we are going to build a Blog API with node and Express, where I'll walk you through every step and at the end of this tutorial, you would have learnt:

  • how to build a fully functional CRUD ( CREATE , READ , UPDATE , DELETE ) application.

  • how to start up a NodeJS application

  • how to protect your route from unauthorized users (JSON Web Token pack will be used).

  • log handling with Joi validator.

  • how to manage calls to our API with rate-limit.

  • how to send and retrieve data from MongoDB.

  • how to handle logging with morgan and Winston, and more...

PREREQUISITES

To follow this tutorial more effectively, having a basic understanding of JavaScript & ES6 syntax is required.
Next, you need to have the following setup on your PC

  1. Node installed

  2. mongo DB

  3. vscode or any code editor of your choice

  4. Any terminal emulator of your choice (Optional)

What is node.js?

Node.js is a JavaScript runtime that allows JavaScript to be used outside of the browser & on the server.
It is primarily deployed for non-blocking, event-driven servers, such as traditional websites and back-end API services.

Now, let's verify that node.js was installed properly. Run this command on your terminal emulator.

node --version

If it returns a version and no error, it means your installation was successful.

Image description

Node Application Set up

  • Create an empty folder BLOG_API and on your terminal, navigate to that folder.

  • Enter the command npm init -y (NB: this is for NPM. Use the command suitable for the package manager you are using).

npm init -y

This command automatically creates a package manager file.

Image description

Now inside the created package.json file I edited author, description and main (from index.js to app.js), you can leave the main (which is your entry point) as the default index.js it doesn't matter. Throughout the article, more updates will be made to this file.

Image description

Now let's create a file that matches the name app.js.

touch app.js

Next, we install express

npm i express

At this point your Application folder setup should be like this:

Image description

NB: Don't try to edit package-lock.json and node_modules.

Setting Up Our Server.

Now, that we have installed express.js, we can set up our server

  • In our app.js file

Image description

  • On the terminal
node app.js

Image description

Yup! our setup was successful. 👌✔

Installing Dependencies

Before we continue let's install some packages that we will use in this project.

Installing dev dependencies

npm i --save-dev nodemon

Installing our remaining Dependencies

npm i  bcrypt dotenv express-rate-limit helmet jsonwebtoken mongoose morgan morgan-json winston
  • Express.js: is a minimal and flexible Node.js web application framework.

  • nodemon : is a command line tool that helps speed up the development of Node.js applications as it monitors and automatically restarts your node application whenever it detects any saved changes.

  • bcrypt: is Used to hash and salt passwords securely. In simple terms, it is used to encrypt passwords before saving them in the database.

  • dotenv: It'll be used to load our secret values/keys (environment variables) from a .env file where we are going to store them.

  • express-rate-limit : helps to monitor and manage requests made from the same IP address, which helps to prevent attacks like brute force.

  • Helmet : It is a collection of several smaller middleware functions that set security-related HTTP response headers.

  • jsonwebtoken (JWT): Used to add authorization and authentication. We will use this library to add protection to some specific routes.

  • mongoose : It is an Object Document Mapper (ODM) for MongoDB. We'll use it to connect to our database as well as set up our Schema to model our application, You can read more from the official documentation here

  • morgan, morgan-JSON and Winston : These libraries will be used to handle logging.

Creating a .env and CONFIG file and Setting up nodemon

  • Create a .env file in the root of your project folder to save your environment variables.
touch .env
  • Create a Utils folder, navigate inside the folder and create a config.js file here we'll save the exported environment variables in an object, to be used in any file of our project.
mkdir utils
cd utils
touch config.js

Setting up nodemon

  • Go to the package.json and update the script section

Image of package.json file

  • Go to your terminal and enter this command to initialize nodemon
npm run dev

Image description

Connecting to MongoDB

As indicated at the beginning of this tutorial, we will be using MongoDB as our preferred database.

MongoDB is a NoSQL cross-platform document-oriented database program.

Before we go ahead to set up and connect our mongoose to the database, let us refactor our code to save the PORT in our .env file and reconnect to our express server.

  • go to the .env save the port variable.
PORT = 4000
  • Next, go to your config.js file
require('dotenv').config()

module.exports = {
  PORT : process.env.PORT
   }
  • Then in our app.js file

Image description

Mongoose connection function set up

  • Now we know how to set up our environment variables, let's set up our mongoose to connect to our MongoDB

  • At the root directory of the project create a folder database, then navigate inside the folder and create a file connectDB

mkdir database
cd database
touch connectDB.js
  • In the connect.js file set up your mongoose connection
    NB: There are various ways of setting up a mongoose connection, feel free to use the method you feel more comfortable with.

Image description

  • Next, We need to get the URL of the database we want to connect to, and to do that, we need to signUp/sign in on MongoDB, and complete the process explained here on this freecodecamp article.

  • Just like we did for the PORT, save the link to the .env file and export to the config file.

Image description

  • We need to set up our database connection, and it's advisable to ensure that the connection to the database is successful before connecting to our server.

One way to achieve this is to create an Async-await function that forces the server to wait for the database to connect first.

  • Go to your app.js file and create an asynchronous function and call the function.

Image description

When nodemon restarts the application.

Image description

MVC Architecture

Model-View-controller (MVC) is a software architectural pattern that separates an application into these 3 main logical components.

  • Model : This is the central component and it directly manages the data, logic and rules of the application.

  • View : This is any representation of information.

  • Controller : Accepts input (usually from users) and converts it to commands for the model or views.

As you already know, this is a Backend Project, so our focus will be on the model and controller.

It is a nice approach to structure your code from the beginning, so we'll be separating the functions/controller from the routes.

-Let's create 3 directories in the root folder, with 2 files each inside. As you must have guessed correctly, from the names of the files, the articles.js handles the design of the articles/blogs while the auth/user.js manages authorized/unauthorized users.

controller
   |_articles.js
    _authController.js

model
   |_articles.js
    _user.js

route
   |_articles.js
    _auth.js

Creating our Home and User Routes.

NB: We will be using “Postman” For our API testing.

Home page

  • Go to the app.js file, enter the details in the image below.

  • Ensure your server is successfully connected.

  • open postman (Or if you are using thunder client extension.

  • enter the http://localhost:4000 and click send.

Image description

Setting up User authorization/authentication route

Before we start working on our blog routes, let's create the user routes, where users can:

  1. Sign Up/Register an Account

  2. Sign In/Login into their registered Account.

  3. Update/Edit their Account.

  4. Delete their registered Account.

  • Go to the './route/auth.js' file
const express = require('express');
const authRoute = express.Router()

//authentication route config
const authenticateUser = require('../utils/authMiddelware')

//register a new user
// authRoute.post('/register', registerUser )

//sign in a registered user
authRoute.post('/login', loginUser )

// Update user Account
authRoute.patch('/update/:userID', updateUser )

//Delete user Account
authRoute.delete('/delete/:userID', deleteUser )

module.exports = authRoute;

For a potential user/client to be able to login to our application we have to ensure that the user has been authenticated and even when the user has been authenticated, we have to verify if they are authorized to visit any of the routes they attempt to access.

Authentication *verifies a user's identification from the credentials stored which the user provided when signing up and then uses those credentials to confirm the user's identity.*The authorization process only begins if a user was successfully authenticated, It mostly checks to verify if a user has some specific access.

JSON Web Token (JWT)

We are going to use JWT as a 'middleware' to implement the authentication/authorization process.
JWT is used to securely transfer information over the web. It consists of 3 parts. A header, payload & signature, and is mostly used for authentication.

(NB: later in this tutorial we'll see how to implement this).

Middleware

Everything is express.js is Middleware.

So middleware functions are very important.
Middlewares are functions that have access to the request and response objects and the next function in the app's request-response cycle. In many cases, the middleware doesn't end the request-response cycle and that is why next() is important as it passes the control to the next function.
NB: failure to call the next() in this scenario, will leave the application hanging.

  • The syntax for middleware is
app.get(path, `(req, res, next) => {}`, (req, res) => {})

Setting up User Schema and Model

  • Go to "./model/user.js file to create the user schema.

  • We require/import the mongoose library.

  • JWT as stated earlier will be used for authentication,
    JWT_SECRET and JWT_LIFETIME are environment variables, JWT_SECRET can be a set of alpha-numeric characters we generate e.g xcfdsaqwergege23xdfegege233 while for JWT_LIFETIME we will be setting the expiration of the token to 1h (1 hour).
    bcrypt will be used to encrypt the user's password before it is saved to the database. (note: it is bad practice to save the user's password in plain text).

      const mongoose = require('mongoose')
      const jwt = require('jsonwebtoken')
       const bcrypt = require('bcrypt')
    
      const Schema = mongoose.Schema
    
      const { JWT_SECRET, JWT_LIFETIME } = require('../utils/config')
    
  • Next, we build our schema

    
      const ArticleSchema = new Schema({
          title:{
              type : String,
              required: [true, "Please include a title to your article"],
              unique : [true, "title already exists, please try another title"],
              lowercase : true
          },
          description :{type : String },
          author: {
              type : mongoose.Schema.Types.ObjectId,
              ref : 'User',
              required : true,
          },
          content : {
              type: String,
              required: [true, " No content added"]
          },
          tags : {
              type : String,
              default: "Others",
              lowercase : true
          },
          state :{
              type : String,
              enum : ["draft", "published"],
              default : "draft",
              lowercase : true
          },
          read_count : {
              type : Number,
              default :0
          },
          reading_time: {
              type: String
          }
    
      }, {  timestamps: {
          createdAt: 'created_at',
          updatedAt: 'updated_at'
        }}
        )
    
      const ArticleModel  = mongoose.model('Article', ArticleSchema);
      module.exports = ArticleModel
    

Next, we add the pre-hook mongoose middle function save, to enable encrypting the password, before saving it to the database.

//pre-hook function to automatically hash users password before its saved to the database
UserSchema.pre('save', async function(next) {

   const salt = await bcrypt.genSalt(10);
   const hashpassed = await bcrypt.hash(this.password, salt);

   this.password = hashpassed;
   next()
})

Next, we'll create 2 Mongoose methods functions.

  1. The first, I named generateJWT, will generate a '1 hour' valid token whenever a user successfully registers or logs into the application.

  2. The second isValidPassword will be called when a user tries to log in their password. It uses bcrypt compare method, to compare the encrypted password with the password the user is signing in with to see if it matches.

//mongoose schema method to generate a token when a user  registers or logs in successfully
UserSchema.methods.generateJWT = function () {
   return jwt.sign({userID : this._id, email : this.email, first_name : this.first_name, last_name : this.last_name },
      JWT_SECRET,
      { 
         expiresIn : JWT_LIFETIME || '1h' 
      }
      );
}

//this method  when called,it  ensures that  users trying to login, enter their correct passowrd,...
//...so this function is necessary since the password in the database in encrypted
UserSchema.methods.isValidPassword = async function(password ){
   const compare = await bcrypt.compare(password, this.password);
   return compare
}

Next, we create the mongoose model, with 2 arguments, the first will be the name of the collection on the MongoDB database
NOTE: MongoDB will automatically pluralize the collection name. So 'User' will be saved as 'users'.
Then, we export


const UserModel = mongoose.model('User', UserSchema);
module.exports = UserModel;

Setting up the JSON web token (JWT) authentication middleware

Remember in our user "./model/user.js file, we set up JWT to create used the user's payload and secret key to create a token, whenever they sign up or sign in to the application.

Now, we also use JWT to set up our authentication middleware, which will be capable of verifying the user from the token provided.

  • Go to the utils folder and create a new file './utils/authMiddleware.js

    ```javascript const jwt = require('jsonwebtoken') const { JWT_SECRET } = require('./config')

const authenticate = (req, res, next) => {
    const authHeader = req.headers.authorization;
    if(!authHeader  ||  ! authHeader.startsWith('Bearer ')){
        throw new Error('Invalid Authentication')
    }


    const token = authHeader.split(" ")[1];

    try {
        const payload = jwt.verify(token, JWT_SECRET)

        //useful when attaching the user[author] to the protected articles route
        req.user = {userID : payload.userID, email : payload.email, first_name : payload.first_name, last_name : payload.last_name}
        next()

    } catch (error) {
        throw new Error('Invalid Authentication')

    }
}

module.exports = authenticate
```

NB: One additional advantage of this authenticate function set-up is that when it is used as a middleware in any of our routes, then from that route, we'll have access to the details of the signed-in user. This will be very important later in this tutorial when we want to automatically save articles with the name of their creators by 'referencing' from the user model.

image showing an example of mongoose reference in use

Updating Our user routes to protect specific routes

  • Go to the './route/auth.js file'

    ```javascript const express = require('express'); //authentication route config const authenticateUser = require('../utils/authMiddelware') const authRoute = express.Router() const {

      registerUser,
      loginUser,
      updateUser,
      deleteUser,
      } = require('../controller/authController');
    
//register a new user
authRoute.post('/register', registerUser )

//sign in a registered user
authRoute.post('/login', loginUser )

//User edits their account
authRoute.patch('/update/:userID', authenticateUser, updateUser )

//user deletes account
authRoute.delete('/delete/:userID',authenticateUser, deleteUser )


module.exports = authRoute;
```

Setting up User/auth Controller

Now, we have our user routes and model set up and JSON web token set up, let's set up the controller functions.

  • Go to the './controller/authController' route

Firstly we import/require the User Model we just created

const UserModel = require('../models/User');

Now let's create the controller functions for our 4 routes

const UserModel = require('../models/User');

//sign up ~ register

const registerUser = async(req, res) =>{
    const {first_name, last_name, email, password} = req.body;

    if(!first_name|| !last_name || !email || !password){

           return res.status(400).json({ message : `Please Enter all required fields`})
       }

    try {
       //check if the potential new User's email already exist
       const emailAlreadyExist = await UserModel.findOne({ email });

       if(emailAlreadyExist){
           return res.status(401).json({msg : `This email '${email}' already exist... try a different email`})
        }

       const user = new UserModel({
               first_name : first_name,
               last_name : last_name,
               email : email,
               password : password //nb: mongoose userSchema pre-hook already handled the password encryption
           })
           const token = user.generateJWT() //called this function from the userSchma mongoose method
           await user.save()

       res.status(201).json({
           success : true,
           message : 'Registration Completed!!!',
           user:{
               id : user._id,
               first_name : user.first_name,
               last_name : user.last_name,
               token
           }
       })
    } catch (error) {
       res.status(500).json({ message : error.message })
    }
}


//signin ~ login

const loginUser = async(req, res) =>{
    try {
        const { email, password} = req.body;

        if(!email || !password){
            return res.status(400).json({ message : `Please provide your email and password`})
        }
        const user = await UserModel.findOne({ email})

        if(!user){
            return res.status(401).json({message : `Invalid Credentials`})
        }

        const validate = await user.isValidPassword(password);

        if(!validate){
            return res.status(401).json({message: `Incorrect password! Please try again`})
        }

        const token = user.generateJWT() //called this function from the userSchma mongoose method

        return res.status(200).json({
            success : true,
            message : 'logged in successfully',
            user : {
            id : user.id,
            token
            }
         })

    } catch (error) {
        res.status(500).json({ message : error.message })
    }
}


//function for users to edit/update their user Account

const updateUser = async(req , res) =>{
        //grabs the user's id from token
        const userID  = req.user.userID;
        const { first_name, last_name, email, password } = req.body;

        try {
            if(userID !== req.params.userID){
                return res.status(401).json({messsage : 'Unauthorized'})
             }

        const user = await UserModel.findOne( { _id: userID })

            if(!user ) {
                return res.status(401).json({messsage : 'Unauthorized'})
            }

                user.first_name = first_name
                user.last_name = last_name
                user.email = email
                user.password = password

            await user.save()
            return res.status(200).json(
                {success : true,
                 message : `Your account has been updated successfully`,
                 user: {
                    first_name : user.first_name,
                    last_name : user.last_name,
                    userID : user._id,
                    email : user.email,
                    password : user.password

                    }
                })

        } catch (error) {
            res.status(500).json({status : false, message : error.message})
        }

}


//Delete user account
const deleteUser = async (req, res) => {
      //grabs the user's id from token
      const userID  = req.user.userID;

    try {
        if(userID !== req.params.userID){
            return res.status(401).json({messsage : 'Unauthorized'})
         }

        const user = await UserModel.findOne({ _id : userID})

        if(!user) {
            return res.status(401).json({messsage : 'Unauthorized'})
         }

         await user.delete()
        return res.status(200).json({ success : true, message: `Your account has been successfully deleted`})

    } catch (error) {
        res.status(500).json({status : false, message : error.message})
    }
}
  • Lastly, we export the controller functions with module.exports to make it possible for us to require and use in our route section.

      module.exports = {
          registerUser,
          loginUser,
          updateUser,
          deleteUser
      }
    

Blog/article routes

These are the article routes we will be working on, and if you noticed it covered the CRUD operation

  • CREATE -POST method

  • READ - GET method

  • UPDATE - PATCH method

  • DELETE - DELETE method

  • Go to the './route/articles.js' file and create your unprotected and protected routes.

    ```javascript const express =require('express');

    //authentication route config const authenticateUser = require('../utils/authMiddelware') //calling the router function const router = express.Router()

    //importing the CRUD functions from the controller folder const {

      createArticle,
      getMyArticles,
      getAllArticles,
      getArticle,
      updateArticle,
      deleteArticle
    

    } =require('../controller/articles')

    //create a new blog post router.post('/new', authenticateUser, createArticle)

    //get all the published aritcles router.get('/', getAllArticles);

    //Requires authentication to enable the Owner get a list of their articles router.get('/my-articles', authenticateUser, getMyArticles)

    //get a published aritcle id : {blogID} router.get('/:blogID', getArticle);

    //Edit/Updating an article router.patch('/:blogID', authenticateUser, updateArticle)

    //delete an article router.delete('/:blogID', authenticateUser, deleteArticle)

module.exports = router
```

Setting up Blog/article Schema and Model

Before we set up the controllers to enable us to make our routes functional, we have to create a mongoose Schema. This will determine how our data will be stored in our database.

  • Go to the './model/articles.js

  • Similarly to the user model, we require the mongoose library and set up our Schema and then we export.


const mongoose =require('mongoose')

const ArticleSchema = new mongoose.Schema({
    title:{
        type : String,
        required: [true, "Please include a title to your article"],
        unique : [true, "title already exists, please try another title"],
        lowercase : true
    },
    description : {type : String },
    author: {
        type : mongoose.Schema.Types.ObjectId,
        ref : 'User',
        required : true,
    },
    content : {
        type: String,
        required: [true, " No content added"]
    },
    tags : {
        type : String,
        default: "Others",
        lowercase : true
    },
    state :{
        type : String,
        enum : ["draft", "published"],
        default : "draft",
        lowercase : true
    },
    read_count : {
        type : Number,
        default :0
    },
    reading_time: {
        type: String
    }

}, {  timestamps: {
    createdAt: 'created_at',
    updatedAt: 'updated_at'
  }
})

const ArticleModel  = mongoose.model('Article', ArticleSchema);
module.exports = ArticleModel

In the author field, we will notice a ref option having a value 'User' . This ref option tells Mongoose which model to use during population, in our case, it is the 'User' model, it also has a special mongoose type .ObjectId that store id from the 'User' model.

In the reading_time field, we are to come up with an algorithm. We'll create a function that will help us calculate the reading_time of each article by calculating the words. This will be done in our POST route for creating a new post router.post('/new', authenticateUser, createArticle)

In the read_count field, we'll calculate the number of times an individual article/blog post has been read, by incrementing it by 1, anything that article is searched. This will be done in our GET route to search for an article router.get('/:blogID', getArticle)

Setting up Blog/article Controller

Now let's set up Our blog/article controller

  • Go to the './controller/articles.js' file
const ArticleModel = require("../models/articles");


//-----------------function to create an article on the blog site-----------------

const createArticle = async (req, res) =>{
     //grabs the user's id from the token in the JWT authorization middleware
     req.body.author = req.user.userID;


   //  creating a function to calculate the reading time
      const calcReadingTime = (text) => {
      const wordPerMinute = 200  //assumes that it takes 200 words to read/min (ref: google)
      const numOfWords = text.split(/\s/g).length //uses regex to count words by white-spaces, newlines
      const mins = (numOfWords/ wordPerMinute) * 1
      const readTime = Math.ceil(mins) //aprox. to the next whole Number

      return `${readTime} minute(s) read`
     }

     try {

                const newArticle = await new ArticleModel({
                 title : req.body.title,
                 description : req.body.description,
                 author : req.body.author,
                 content: req.body.content,
                 tags : req.body.tags,
                 read_count: req.body.read_count,
                 reading_time : calcReadingTime(req.body.content),
                  })

            const article = await newArticle.save()
            return res.status(201).json({status : true,  article });

   } catch (error) {
      res.status(500).json({status : false, message : error.message})
   }

}

//------------displaying all the published articles on the blog---------------------

const getAllArticles= async(req, res) =>{

   try {

         const { author, title, tags, sort } = req.query;

         const findQuery = {
            state : "published"
             //This 'hard coded line ensures on 'published' are displayed on this route
         };

            //Q-14b. --searchable(filter) by the author, Blog's title & tags
         if( author ){
            findQuery.author = author
         }
         if( title ){
            findQuery.title = { $regex : title, $options : 'i' }
            //using regex : enables us to search for titles even with the incomplete word [e.g  'ok' ==> 'look' & 'okay'];
            //adding the 'i' options : is to make our search words to be 'case Insensitive'
         }
         if( tags ){
            findQuery.tags = tags
         }

         let result =  ArticleModel.find(findQuery)

         // ..it should be orderable .......
         if(sort){
            const sortListItems = sort.split(',').join(' ');
            result = result.sort(sortListItems)
            //this dynamically sorts the list with different available items
         }else{

            result = result.sort('-read_count ,reading_time, -created_at')
            //by default, this sorts by the read_count..
            // [highest-lowest] --> reading_time[shortest - longest] --> created_at[newest - oldest]

         }

           // paginate (default 20 blogs per page)
         const page = parseInt(req.query.page)  || 1
         const limit = parseInt(req.query.limit) || 20
         const skip = (page - 1) * limit
         //this skip logic ensures that the results starts from the actual first item,
         //''' it also ensures that the next page starts from the next item and not the last item of the previous result

         result = result
         .skip(skip)
         .limit(limit)
         .populate({
            path : 'author',
            select :{
               first_name : 1,
               last_name : 2
               }
         })

          const article = await result
         return res.status(200).json({ status : true, article, count : article.length})

   } catch (error) {
    res.status(500).json({status : false, message : error.message})
 }
}

   //--------------displaying Onwer's Articles--------------------

      const getMyArticles = async( req, res) =>{

         //Owners blogs should be paginated
      const page = Number(req.query.page)   || 1    //page 1:default
      const limit = Number(req.query.limit) || 10    //10 per page
      const skip = (page - 1) * limit
      try {
         const article = await ArticleModel
         .find({author : req.user.userID})
         .sort('-state')
         .skip(skip)
         .limit(limit)

         return res.status(200).json({status : true, article, count: article.length})
      } catch (error) {
         res.status(500).json({status : false, message : error.message})
      }
   }


//----------------search and GET an article--------------------
const getArticle = async(req, res) =>{
   const {blogID} = req.params;

    const article  = await ArticleModel.findOne({ _id : blogID})
   .populate({
      path : 'author',
      select :{
         first_name : 1,
         last_name : 2,
         email : 3
         }
      //Q-15a. I used 'populate' here to generate additional information about the author
   })

   if(!article){
      return res.status(404).json({status : false, message: `No article with the ID: ${blogID}  found!`})
   }
   //Q-15b. increments a blog post by 1 everytime it is read/requested
   article.read_count ++
   try {
      await article.save()
      return res.status(200).json({success : true , article  })


    } catch (error) {
      res.status(500).json({status : false, message : error.message})
    }

}


//Editing or Updating an article
const updateArticle = async(req, res) =>{
     //grabs the user's id from token
      req.body.author  = req.user.userID;

      const { blogID } = req.params;
      const { body }= req;

      if(body ===" "){
         throw new Error('fields cannot be left empty')
      }

      try {
         const article = await ArticleModel.findOneAndUpdate({_id: blogID, author : req.user.userID}, body,{
               new: true,
               runValidators:true
           });

      if(!article){
         return res.status(401).json({status : false, msg: `You're NOT authorized to Edit this article ID:${blogID}`})
      }

      res.status(200).json({success : true, article })

   } catch (error) {
      res.status(500).json({status : false, message : error.message})
   }
}


//Deleting an article
const deleteArticle = async(req, res)=>{

   //grabs the user's id from token
   req.body.author  = req.user.userID;
   const {blogID} = req.params;

try {

      const article = await ArticleModel.findOneAndRemove({_id: blogID, author: req.body.author})


      if(!article){
         return res.status(401).json({status : false, msg: `You're Unauthorized to delete this article ID:${blogID}`})
      }

      res.status(200).json({success : true, message :`Successful! Your          Article has been Deleted!`})
   } catch (error) {
      res.status(500).json({status : false, message : error.message})
   }
}

module.exports={
    createArticle,
    getArticle,
    getAllArticles,
    getMyArticles,
    updateArticle,
    deleteArticle,
}

Apart from creating our Article routes and successfully implementing the logic we talked about earlier on, you'll notice we added some other features like pagination and we also used the .populate property to add more details about the authors to their blogs.

Updating Our Entry point app.js

  1. router middlewares

  2. helmet (adds security)

  3. express-rate-limit

  4. express JSON and URL encoded (enables our application to have access to user's input)

  5. invalid '404' handler (manages any undefined route)

const express = require('express') 
const rateLimit = require('express-rate-limit')
const helmet = require('helmet')
//importing the mongoDB dataBase configuration file
const connectDB = require('./database/connectDB')
const app = express();
const { PORT, MONGO_DB_URI } = require('./utils/config')
//route for register & signIn
const authUser = require('./route/auth')
//blog route config
const  articleRouter = require('./route/articlesRoute');

//Security
app.use(helmet())


const limiter = rateLimit({
    windowMs: 10 * 60 * 1000, // 10 minute
    max: 80, // Limit each IP to 80 requests per `window` (here, per 10 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)


//middleware to grab post/patch requests as json files or other files
app.use(express.json())
app.use(express.urlencoded({extended : true}))


//home page
app.get('/',(req, res) =>{
  res.send({
    success : true,
    message : "Welcome to My Blog HomePage ",
    "Article route" :  "/api/v1/articles"
  })
})

//app middleware to the user registration & login routes
app.use('/api/v1/auth', authUser )

//app middleware to the blog article route
app.use('/api/v1/articles', articleRouter)


//handling invalid (404) routes
app.all('*',  (req, res) =>{
    res.status(404).json({ message : "This route does not exist"})
})

//setting up the Database and server connections
const start = async() =>{
   try{
       await connectDB(MONGO_DB_URI)
       console.log('database connected')
       app.listen( PORT, () => {console.log(`Server listening at port: ${PORT}`) })

     }catch (error) {
       console.log('Unable to connect to the Database ' + error)
  }
}

//starts the connections
start();

Handling Logs with Morgan and Winston

Application logging is a critical part of log management and can help keep your business running smoothly and securely.

Application logging is the process of saving application events. With this information in hand, you can assess threats and analyze errors before they disrupt broader business workflows.

We're going to combine Winston and morgan (with the help of another library Winston-JSON) to handle our logging.

Setting up Winston

  • Create a new folder logger in the root folder, navigate into it and create a file logger.js.

      mkdir logger
      cd logger
      touch logger.js
    
  • next, we require the Winston library

    Winston is a multi-transport async logging library for Node.js. A transport is essentially a storage device for your logs.

    What this means is that with Winston you can save different levels (or all logs ) at different locations.

    Image displaying logging levels

  • Note that npm logging levels are prioritized from 0 to 6 (highest to lowest) and if you specify a level to be logged, it logs that level and all levels below (for example. if you set log for HTTP, it will log HTTP, info, warn and error levels)

  • In this tutorial, we will set our 'info' logs to save to a file './log/app.log' while setting our 'debug' logs to display on the console.

Now, let us require/import Winston

const winston = require('winston')

Next, let's create our Winston transport


const options = {
  file : {
    level : 'info',
    filename : './log/app.log',
    handleExceptions : true,
    json : true,
    maxsize : 5242880, //5MB
    maxFile : 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
  • Next, in the logger folder let's create a second file httpLogger.js

We require the morgan and morgan-json library and also require the logger that was created with Winston.

const morgan = require('morgan')
const json  =require('morgan-json')
const logger = require('./logger')

Next, we defined the format using morgan-json

const format = json({
  method :':method',
  url : ':url' ,
  status : ':status',
  contentLength: ':res[content-length]',
  responseTime: ':response-time ms'
});

Using Winston in morgan

const httpLogger= morgan(format,{
  stream: {
    write: (message) => {
      // winston.info(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

Updating Our Entry point app.js with logs

  • we will import the httpLogger ( morgan-Winston ) file and use it as middleware.

we will import the logger ( Winston ) file, and use it to update our console logs

console.log  --> logger.info, logger.error

Our Updated app.js file

const express = require('express') 
const rateLimit = require('express-rate-limit')
const helmet = require('helmet')
//importing the mongoDB dataBase configuration file
const httpLogger = require('./logger/httpLogger')
//winston logger
const logger = require('./logger/logger')
const connectDB = require('./database/connectDB')
const { PORT, MONGO_DB_URI } = require('./utils/config')
const app = express();


//route for register & signIn
const authUser = require('./route/auth')
//blog route config
const  articleRouter = require('./route/articlesRoute');

//Security
app.use(helmet())
//morgan/winston logger
app.use(httpLogger)

const limiter = rateLimit({
    windowMs: 10 * 60 * 1000, // 10 minute
    max: 80, // Limit each IP to 80 requests per `window` (here, per 10 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)


//middleware to grab post/patch requests as json files or other files
app.use(express.json())
app.use(express.urlencoded({extended : true}))


//home page
app.get('/',(req, res) =>{
  res.send({
    success : true,
    message : "Welcome to My Blog HomePage ",
    "Article route" :  "/api/v1/articles"
  })
})

//app middleware to the user registration & login routes
app.use('/api/v1/auth', authUser )

//app middleware to the blog article route
app.use('/api/v1/articles', articleRouter)


//handling invalid (404) routes
app.all('*',  (req, res) =>{
    res.status(404).json({ message : "This route does not exist"})
})

//setting up the Database and server connections
const start = async() =>{
   try{
       await connectDB(MONGO_DB_URI)
       logger.info('database connected')
       app.listen( PORT, () => { logger.info(`Server listening at port: ${PORT}`) })

     }catch (error) {
       logger.error('Unable to connect to the Database ' + error)
  }
}

//starts the connections
start();
  • Now, when we reconnect to our servers, we will notice that the logging level alongside the display message is displayed in a JSON format. We'll also notice that a file with our log file './log/app.log' was automatically created because of our configuration in the Winston file.

    Now let's visit an invalid route http://localhost:4000/articles and then our valid blog route that displays all the published articles http://localhost:4000/api/v1/articles

  • We will notice more information in our log, (like the express method, status code, date-time e.t.c) as defined with our morgan-json and morgan set up.

Wrap Up

We have come to the end of this tutorial, and we have been able to build an excellent Node.js Blog API, where Users can Sign up, Create and publish their own blog posts/articles, Edit and delete their articles, as well as Update and delete their account details.

I hope you learnt a lot from this tutorial as well as the links attached for a deeper understanding. In case you missed it, Here's another link to the repo on GitHub.