How to Build a blog API with Node.js

How to Build a blog API with Node.js

Introduction

A blog API allows client applications to create, view, edit, delete and even filter blog content. In this article, we are going to create a simple blog API using Node.js, Express.js, and MongoDB

Prerequisite

A basic understanding of the following:

  • Node.js

  • Javascript

  • Express.js

make sure you install the following on your computer:

  • Node.js

  • Code Editor (eg VS code)

API Structure

Our API will have the following endpoints

Unprotected Routes

MethodRouteFunction
GET/Return Homepage
GET/articlesReturn all published articles
GET/articles/:articleIdReturn a single article by ID
POST/sign-upReturn Homepage
POST/loginReturn all published articles

Protected Routes

MethodRouteFunction
GET/user/articlesReturn all articles created by the user
POST/create-articleCreate a new article
PUT/edit-article/:articleId/update-stateUpdate the state of an article
PUT/edit-article/:articleIdEdit article
DELETE/delete-article/:articleIdDeletes an article

Implementation

Project Setup

  1. Create a new folder and open it in your code editor

  2. Go to your terminal, navigate to the folder you created and execute the following command npm init -y

  3. Install express package npm i express

  4. Create an ‘index.js’ file, inside this file require an express Module, and create a constant ‘app’ for creating an instance of the express module. then export app

     const express = require("express");
    
     const app = express();
     module.exports = app;
    
  5. Install mongoose package npm i mongoose

  6. Sign in to https://www.mongodb.com/ or create an account if you don't have one.

  7. Create a new Project, Cluster, User, and add your IP to the IP access list. If you are not sure how to do that visit https://www.mongodb.com/docs/manual/tutorial/getting-started/

  8. Click on connect to get your connection string, it should look like this
    mongodb+srv://<user>:<password>@cluster0.00o4wev.mongodb.net/blog?

  9. create a '.env' file to store our environment variable, inside this file add your database connection string and name it MONGODB_URI MONGODB_URI=mongodb+srv://<user>:<password>@cluster0.00o4wev.mongodb.net/blog?

  10. Create a folder, name it 'utils' and create a 'database.js' file. add the following code inside database.js

    const mongoose = require('mongoose');
    
    const connect = (url) => {
        mongoose.connect(url || 'mongodb://localhost:27017')
    
        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
    };
    
  11. Create an 'app.js' file, inside the file require index.js, database.js, and dotenv.
    Read MONGODB_URI from the .env file

    Then call connect() function from database.js and app.listen() from the index.js passing the MONGODB_URI and PORT number respectively

    const app = require('./index');
    require("dotenv").config();
    
    const Database = require("./utils/database");
    
    const PORT = 8080
    const MONGODB_URI = process.env.MONGODB_URI;
    
    Database.connect(MONGODB_URI);
    
    app.listen(PORT, () => {
      console.log(`server running on  http://localhost:${PORT}`);
    });
    

Authentication

Routes

In this section, we will implement user sign-up and log-in, we will make use of passport - an authentication middleware for Node.js

  1. install the following packages npm I passport passport-jwt passport-local jsonwebtoken

  2. Create a 'routes' folder and create an 'auth.js' file inside the folder.

  3. Inside auth.js, import express, passport, jsonwebtoken and dotenv

     const express = require("express");
     const passport = require("passport");
     const jwt = require('jsonwebtoken');
     require('dotenv').config();
    
  4. Create a const ' router'

     const router = express.Router();
    
  5. Implement the sign-up route as shown below

     router.post(
       "/sign-up",
       passport.authenticate("signup", { session: false }),
       async (req, res, next) => {
         res.status(201).json({
           message: "Signup successful",
           user: req.user,
         });
       }
     );
    
  6. add JWT_SECRET - 'a random string' to the '.env' we created earlier, It will be used in validating jwt tokens

    JWT_SECRET=djnisdnrosdnfodjnfonsdfnsd

  7. Implement the login route as shown below

     router.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("Username or password is incorrect here");
             return next(error);
           }
    
           req.login(user, { session: false }, async (error) => {
             if (error) return next(error);
    
             const body = { _id: user._id, email: user.email };
    
             const token = jwt.sign({ user: body }, process.env.JWT_SECRET, { expiresIn: '1h' });
    
             return res.json({ token });
           });
         } catch (error) {
           return next(error);
         }
       })(req, res, next);
     });
    
  8. export the router module.exports = router;

  9. Import it into index.js and add the route

     const authRoute = require("./routes/auth");
    
     app.use("/", authRoute);
    

Model

  1. Create a 'models' folder and a 'user.js' file

  2. import mongoose const mongoose = require("mongoose");

  3. Create a const 'Schema' const Schema = mongoose.Schema;

  4. Create the userSchema

const userSchema = new Schema({
    email: {
        type: String,
        required: true,
        unique: true
      },
    first_name: {
        type: String,
        required: true,
      },
    last_name: {
        type: String,
        required: true,
      },
    password: {
        type: String,
        required: true,
      },
});
  1. Install bycrypt, It will be used to hash and validate password `npm I bycrypt'

  2. Add pre-hook - Processing scripts that hashes the password before saving it to the database

userSchema.pre(
  'save',
  async function (next) {
      const user = this;
      const hash = await bcrypt.hash(this.password, 12);

      this.password = hash;
      next();
  }
);
  1. Add isValidPassword method, we will use it to validate password
userSchema.methods.isValidPassword = async function(password) {
  const user = this;
  const compare = await bcrypt.compare(password, user.password);

  return compare;
}
  1. Export the model module.exports = mongoose.model("User", userSchema);

Controller

  1. Create a 'controllers' folder and create an 'auth.js' file

  2. Inside auth.js, import passport, passport-local, passport-jwt and our user model

     const passport = require("passport");
     const localStrategy = require("passport-local").Strategy;
     const JWTstrategy = require("passport-jwt").Strategy;
     const ExtractJWT = require("passport-jwt").ExtractJwt;
    
     const User = require("../models/user");
    
  3. Implement Jwt Streategy

     passport.use(
       new JWTstrategy(
         {
           secretOrKey: process.env.JWT_SECRET,
           jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
         },
         async (token, done) => {
           try {
             return done(null, token.user);
           } catch (error) {
             done(error);
           }
         }
       )
     );
    
  4. Implement the sign-up logic

     passport.use(
       "signup",
       new localStrategy(
         {
           usernameField: "email",
           passwordField: "password",
           passReqToCallback: true,
         },
         async (req, email, password, done) => {
           try {
             const first_name = req.body.first_name;
             const last_name = req.body.last_name;
    
             const user = await User.create({
               email,
               password,
               first_name,
               last_name,
             });
    
             return done(null, user);
           } catch (error) {
             done(error);
           }
         }
       )
     );
    
  5. Implement the login logic

     passport.use(
       "login",
       new localStrategy(
         {
           usernameField: "email",
           passwordField: "password",
         },
         async (email, password, done) => {
           try {
             const user = await User.findOne({ email });
    
             if (!user) {
               return done(null, false, { message: "User not found" });
             }
    
             const validate = await user.isValidPassword(password);
    
             if (!validate) {
               return done(null, false, { message: "Wrong Password" });
             }
    
             return done(null, user, { message: "Logged in Successfully" });
           } catch (error) {
             console.log(error);
             return done(error);
           }
         }
       )
     );
    

Now we are done with the authentication section, a user can sign up and log in, moving on we will work on implementing the function a logged-in user can perform

User

Model

  1. create a article.js file in the model folder

  2. Import mongoose const mongoose = require("mongoose");

  3. create a constant, Schema const Schema = mongoose.Schema;

  4. create articleSchema

     const articleSchema = new Schema({
       title: {
         type: String,
         required: true,
         unique: true
       },
       description: String,
       author: {
         type: Schema.Types.ObjectId,
         ref: 'User',
         required: true
       },
       state: {
         type: String,
         enum: ["draft", "published"],
         default: "draft",
         required: true
       },
       read_count: {
         type: Number,
         default: 0,
         required: true
       },
       reading_time: String,
       tags: [String],
       body: {
         type: String,
         required: true,
       },
       timestamp: {
         type: Date,
         default: Date.now,
         required: true,
       },
     });
    
  5. export the model module.exports = mongoose.model("Article", articleSchema);

Controllers

  1. Create a 'user.js' file in the controllers' folder

  2. Import the articles model

  3. Implement getArticles logic

     exports.getArticles = async (req, res, next) => {
       const { query } = req;
       const { state, page = 0, per_page = 5 } = query;
    
       const findQuery = { author: req.user };
    
       if (state) {
         findQuery.state = state;
       }
    
       try {
         const articles = await Article
           .find(findQuery)
           .skip(page)
           .limit(per_page)
    
         res.json(articles);
       } catch (err) {
         res.status(500).json({message: "an error occured"});
         console.log(err);
       }
     };
    
  4. Implement postCreateArticle logic

    
     exports.postCreateArticle = async (req, res, next) => {
       const title = req.body.title;
       const description = req.body.description;
       const tags = req.body.tags.split(",").map((tag) => {
         return tag.trim();
       });
       const body = req.body.body;
       const author = req.user;
       const reading_time = calcReadingTime.calcReadingTime(body);
    
       if (await Article.findOne({ title })) {
         res.status(202).json({message : "title already exist, try something different"});
         return;
       }
    
       const article = new Article({
         title,
         description,
         author,
         reading_time,
         tags,
         body,
       });
    
       try {
         await article.save();
         res.status(201).json(article);
       } catch (err) {
         res.status(500).json({message: "an error occured"});
         console.log(err);
       }
     };
    
  5. Implement postEditArticle

     exports.postEditArticle = async (req, res, next) => {
       const updatedTitle = req.body.title;
       const updatedDescription = req.body.description;
       const updatedTags = req.body.tags.split(",").map((tag) => {
         return tag.trim();
       });
       const updatedBody = req.body.body;
       const updatedReading_time = calcReadingTime.calcReadingTime(updatedBody);
       const articleId = req.params.articleId;
    
       if (await Article.findOne({ title: updatedTitle })) {
         console.log(Article.findOne({ title: updatedTitle }));
         res.status(202).json({message : "title already exist, try something different"});
         return;
       }
    
       try {
         const article = await Article.findOne({ _id: articleId, author: req.user });
    
         article.title = updatedTitle;
         article.description = updatedDescription;
         article.reading_time = updatedReading_time;
         article.tags = updatedTags;
         article.body = updatedBody;
    
         await article.save();
    
         res.status(201).json(article);
       } catch (err) {
         res.status(500).json({message: "an error occured"});
         console.log(err);
       }
     };
    
  6. Implement updateState

     exports.postUpdateState = async (req, res, next) => {
       const articleId = req.params.articleId;
    
       try {
         const article = await Article.findOne({ _id: articleId, author: req.user });
         article.state = "published";
         await article.save();
         res.status(201).json(article);
       } catch (err) {
         res.status(500).json({message: "an error occured"});
         console.log(err);
       }
     };
    
  7. Implement delete

     exports.postDeletetArticle = async (req, res, next) => {
       const articleId = req.params.articleId;
    
       try {
         const response = await Article.deleteOne({
           _id: articleId,
           author: req.user,
         });
         res.status(202).json(response);
       } catch (err) {
         res.status(500).json({message: "an error occured"});
         console.log(err);
       }
     };
    

Routes

1. Create a user.js file in the routes folder

2. Import express and create a constant router

3. Import the controller user.js file into the controller folder

javascript const express = require('express') const router = express.Router()

const userController = require('../controllers/user'

4. Add the following routes and pass in the right controller

javascript router.get('/articles', userController.getArticles)

router.post('/create-article', userController.postCreateArticle)

router.put('/edit-article/:articleId/update_state', userController.postUpdateState)

router.put('/edit-article/:articleId', userController.postEditArticle)

router.delete('/delete-article/:articleId', userController.postDeletetArticle)

5. export router module.exports = router

6. Import router into index.js

javascript const userRoute = require("./routes/user");

7. add user route, and pass in passport.authenticate to protect route

javascript app.use("/user", passport.authenticate("jwt",{ session: false, failureRedirect: "/login"}),userRoute);

Blog

We will now implement the endpoints that allowed both logged-in and not logged-in users to access published blogs

Controllers

1. create a blog.js file in the controllers folder

2. Import our articles model Article = require("../models/article");

3. Implement getIndex

javascript exports.getIndex = async (req, res, next) => { res.json({message: "welcome to AltBlog"}) }

4. Implement getArticles

javascript exports.getArticles = async (req, res, next) => { const { query } = req; const { author, title, tags,

order = "asc", order_by = "timestamp",

page = 0, per_page = 20, } = query;

const findQuery = { state: "published" };

if (author) { findQuery.author = author; }

if (title) { findQuery.title = title; }

if (tags) { findQuery.tags = { $in: tags.split(",").map((tag) => { return tag.trim(); }), }; }

const sortQuery = {};

const sortAttributes = order_by.split(",").map((order) => { return order.trim(); });;

for (const attribute of sortAttributes) { if (order === "asc" && order_by) { sortQuery[attribute] = 1; }

if (order === "desc" && order_by) { sortQuery[attribute] = -1; } }

try { const articles = await Article.find(findQuery) .sort(sortQuery) .skip(page) .limit(per_page);

res.json(articles); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };

5. Implement getArticle to return a single article by id

javascript exports.getArticle = async (req, res, next) => { const { articleId } = req.params;

try { const article = await Article.findOne({ _id: articleId, state: "published", }).populate("author", "first_name last_name email");

article.read_count++;

article.save();

res.json(article); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };

Routes

1. create a blog.js file in the routes folder

2. Import express and create a router constant

javascript const express = require('express') const router = express.Router()

3. Import blog.js from the controller file const blogController = require('../controllers/blog')

4. Implement the following routes and pass in the right controller

javascript router.get('/', blogController.getIndex)

router.get('/articles', blogController.getArticles)

router.get('/articles/:articleId', blogController.getArticle)

5. export router module.exports = router

6. Import bog.js into index.js and add the route

javascript const userRoute = require("./routes/user");

app.use("/", blogRoute);

Error Controller

The error handles error and return the right message to the client

1. Create an 'error.js' file in the controllers' folder

2. Add the following function to return 'page not found' when the clients tried to access and route that doesn't exist

javascript exports.error404 = (req, res, next) => { res.status(404).json({ message: '404 page not found' }) }

3. Import it into index.js and add it to the routes

javascript const errorController = require("./controllers/error");

app.use("/", errorController.error404);

Running the API

At this point, we are ready to run our API. to do that go to the terminal and execute node app.js, this will start the app

Conclusion

And this is how you can create a simple blog API with Node.js.

More features such as validation, views, and tests can be added as well