How to build a blog api with Nodejs, ExpressJs and Mongodb.

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.