How I built my blog api

How I built my blog api

Table of contents

No heading

No headings in the article.

In this article, I will guide you through the process of building a simple and secure blog API using Node.js, Express, MongoDB, and several packages including bcrypt, body-parser, cors, dotenv, express, express-async-errors, express-rate-limit, helmet, joi, jsonwebtoken, moment, mongodb-memory-server, mongoose, morgan, morgan-json, passport, passport-jwt, passport-local, swagger-ui-express, winston, and yamljs. The API will have the ability to perform CRUD operations, authentication, rate-limiting, and logging. We will also be using Swagger UI to document the API endpoints and create a user interface for testing.

The first step in building the blog API was to set up the environment by installing all the required packages.

To begin, we need to have Node.js and npm installed on our computers. If you don't already have them installed, you can download them from the official website.

Next, create a new directory and navigate to it in the terminal. Run the following command to create a package.json file:

npm init -y

We will be using various packages for our blog API. To install these packages, run the following command:

npm install bcrypt body-parser cors dotenv express express-async-errors  morgan morgan-json winston swagger-ui-express joi passport passport-jwt passport-local yamljs express-rate-limit helmet

Once all the packages were installed, I started setting up the express framework. Express is a fast and flexible framework for building web applications and APIs.

const express = require("express");

const app = express();

app.get("/", (req, res) => {
    res.send('Welcome to my blog app!');

});

Next, I set up the database connection using mongodb-memory-server and mongoose. Mongodb-memory-server is a package that creates an in-memory instance of MongoDB for testing purposes, and mongoose is an object data modeling (ODM) library for MongoDB. Together, these packages made it easy to connect to the database and perform CRUD operations.

To connect to MongoDB, we will be using mongoose. First, create a new file called db.js and add the following code:

const mongoose = require('mongoose');
const app = require("./app");
const PORT = process.env.PORT || 3334;

async function start() {
    try {
        await mongoose.connect(process.env.MONGO_URI);

        app.listen(PORT, () => {
            logger.info(`server is listening on port, ${PORT}`);
        });
    } catch (error) {
        logger.error(error);
    }
}

start();

I also set up the security of the API by using helmet and cors. Helmet is a package that sets HTTP headers to help secure the application, and cors is a package that enables cross-origin resource sharing. Both packages are essential for securing the API and ensuring that it can only be accessed by authorized users.

const cors = require("cors");
const helmet = require("helmet");

app.use(cors());
app.use(helmet());

The next step is to define the schema of the database to control data entries. Schemas are used to define the structure and constraints of the data that will be stored in a database. They serve as a blueprint that specifies the properties and types of the data, as well as any constraints and validation rules. This was done in a separate folder called models. There I created two new files for both the user schema and the blog post schema.

The blog post schema:

const mongoose = require('mongoose');
const { Schema } = mongoose;

const BlogPost = new Schema(
  {
    title: {
      type: String,
      required: [true, 'Please provide a title for your blog'],
      unique: [true, 'This title already exists!'],
    },
    state: {
      type: String,
      enum: ['draft', 'published'],
      default: 'draft',
    },
    description: String,
    tags: String,
    author: {
      type: String,
      ref: 'User',
    },
    read_count: Number,
    reading_time: Number,
    body: {
      type: String,
      required: [true, 'Please enter text in the body of your blog'],
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model('Blog', BlogPost);

The user schema:

const mongoose = require("mongoose");
const { Schema } = mongoose;
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");

const UserSchema = new Schema({
    first_name: {
        required: [true, "Please enter your first name"],
        type: String,
    },
    last_name: {
        required: [true, "Please enter your last name"],
        type: String,
    },
    email: {
        required: [true, "Please enter your email"],
        type: String,
        unique: [true, "email already exists!"],
        match: [
            /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
            "please provide a valid email",
        ],
    },
    password: {
        required: [true, "Please enter your password"],
        type: String,
        min: [8, "Password is too short"],
    },
});

UserSchema.pre("save", async function (next) {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
});

UserSchema.methods.createJWT = function () {
    return jwt.sign(
        { userId: this._id, email: this.email },
        process.env.JWT_SECRET,
        { expiresIn: process.env.JWT_EXP }
    );
};

UserSchema.methods.comparePassword = async function (password) {
    const isMatch = await bcrypt.compare(password, this.password);
    return isMatch;
};

module.exports = mongoose.model("User", UserSchema);

To validate user input, I used joi, a package that allows developers to validate inputs using an object schema. Joi is a flexible and easy-to-use package that helps ensure that the input received from the client is valid.

A simple auth input validator using joi:

const Joi = require("joi");

const signUpValidator = Joi.object({
    first_name: Joi.string().max(255).required(),
    last_name: Joi.string().required().trim(),
    email: Joi.string().email({
        minDomainSegments: 2,
        tlds: { allow: ["com", "net"] },
    }),
    password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")),
});

const logInValidator = Joi.object({
    email: Joi.string().email(),
    password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")),
});

async function signUpValidationMiddleware(req, res, next) {
    const authorPayload = req.body;
    try {
        await signUpValidator.validateAsync(authorPayload);
        next();
    } catch (error) {
        next({
            message: error.details[0].message,
            status: 400,
        });
    }
}

async function logInValidationMiddleware(req, res, next) {
    const authorPayload = req.body;
    try {
        await logInValidator.validateAsync(authorPayload);
        next();
    } catch (error) {
        next({
            message: error.details[0].message,
            status: 400,
        });
    }
}

module.exports = { signUpValidationMiddleware, logInValidationMiddleware };

I then added an authentification middleware, I used passport and passport-jwt. Passport is a middleware library that simplifies the process of authenticating users, and passport-jwt is a package that allows users to authenticate using JSON Web Tokens (JWT). Together, these packages made it easy to authenticate users and secure the API. The code below shows a simple setup for both login and signup functionalities:

const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy,
  ExtractJwt = require('passport-jwt').ExtractJwt;
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');
const { Unauthorized } = require('../errors/unauthorized');

const jwtSecret = process.env.JWT_SECRET;

var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = jwtSecret;

passport.use(
  new JwtStrategy(opts, async function (jwt_payload, done) {
    try {
      if (jwt_payload) {
        return done(null, jwt_payload);
      } else {
        return done(null, false);
        // or you could create a new account
      }
    } catch (error) {
      done(error);
    }

    // });
  })
);

passport.use(
  'signup',
  new LocalStrategy(
    {
      passReqToCallback: true,
      usernameField: 'email',
      passwordField: 'password',
    },
    async (req, username, password, done) => {
      try {
        const user = await User.create({
          ...req.body,
          email: username,
          password: password,
        });
        if (!user) {
          const error = new Unauthorized('User already exists');
          return done(error);
        }
        return done(null, { user, token: user.createJWT() });
      } catch (error) {
        done(error);
      }
    }
  )
);

passport.use(
  'login',
  new LocalStrategy(
    {
      usernameField: 'email',
      passwordField: 'password',
      passReqToCallback: true,
    },
    function (req, email, password, done) {
      if (email) email = email.toLowerCase();

      process.nextTick(function () {
        User.findOne({ email: email }, function (err, user) {
          if (err) return done(err);

          if (!user) {
            const error = new Unauthorized();
            error.message = 'User not found';
            return done(error);
          }

          if (!user.comparePassword(password)) {
            const error = new Unauthorized();
            error.message = 'Invalid password';
            return done(error);
          } else return done(null, { user, token: user.createJWT() });
        });
      });
    }
  )
);

The next thing to do was set up my controller. This was in line with the MVC engineering pattern. The controller contains the logic for processing incoming requests, making changes to the data in response to the requests, and sending a response back to the client in a separate folder.

const { Unauthorized } = require('../errors/unauthorized');
const Blog = require('../models/Blog');

const getAllBlogs = async (req, res, next) => {
  try {
    const query = req.query;
    console.log(query);
    const queryObject = {};
    if (query.author) {
      queryObject.author = query.author;
    }
    if (query.title) {
      queryObject.title = query.title;
    }

    if (query.tags) {
      queryObject.tags = query.tags;
    }

    const page = +req.query.page || 1;
    const limit = +req.query.limit || 20;
    const skip = (page - 1) * limit;
    const blogs = await Blog.find({ ...queryObject, state: 'published' })
      .skip(skip)
      .limit(limit)
      .sort({
        createdAt: query.createdAt,
        read_count: query.read_count,
        reading_time: query.reading_time,
      });

    res.send(blogs);
  } catch (error) {
    next(error);
  }
};

const getMyBlogs = async (req, res, next) => {
  try {

    const query = req.query;
    const queryObject = {};

    if (query.state) {
      queryObject.state = query.state;
    }

    const page = +req.query.page || 1;
    const limit = +req.query.limit || 20;
    const skip = (page - 1) * limit;

    const myBlogs = await Blog.find({
      ...queryObject,
      author: req.user.userId,
    })
      .skip(skip)
      .limit(limit);

    res.json(myBlogs);
  } catch (error) {
    next(error);
  }
};

const getBlog = async (req, res, next) => {
  try {
    const blog = await Blog.findOne({
      _id: req.params.id,
      state: 'published',
    }).populate('author');

    if (!blog) {
      return res.json({ msg: 'No blog found' });
    }
    const oldReadCount = blog.read_count;
    await blog.update({ read_count: oldReadCount + 1 });
    res.send(blog);
  } catch (error) {
    next(error);
  }
};

const postBlog = async (req, res, next) => {
  try {
    const post = await Blog.create({
      ...req.body,
      read_count: 0,
      reading_time: req.body.body.length / 250,
      author: req.user.userId,
    });
    res.json({ post });
  } catch (error) {
    next(error);
  }
};

const updateBlogPost = async (req, res, next) => {
  try {
    const blog = await Blog.findById(req.params.id);
    if (req.user.userId === blog.author) {
      const update = await blog.updateOne({ ...req.body });
      res.send(update);
    } else
      throw new Unauthorized(
        'You are do not have authorization to update this post '
      );
  } catch (error) {
    next(error);
  }
};

const deleteBlogPost = async (req, res, next) => {
  try {
    const blog = await Blog.findById(req.params.id);
    if (req.user.userId === blog.author) {
      const update = await blog.deleteOne({ _id: req.params.id });
      res.send(update);
    } else
      throw new Unauthorized(
        'You are do not have authorization to delete this post '
      );
  } catch (error) {
    next(error);
  }
};

module.exports = {
  getAllBlogs,
  postBlog,
  updateBlogPost,
  deleteBlogPost,
  getBlog,
  getMyBlogs,
};

The purpose of controllers is to encapsulate the business logic of an application and separate it from the routing and presentation logic.


Naturally, after setting up the controller logic the next step was to set up the routing i.e mapping URLs to specific functions, called "handlers," that are responsible for processing HTTP requests and generating the response. The validation middleware was also added to specific routes that took inputs (the URLs that used the HTTP methods: POST and PATCH)

const { Router } = require("express");
const express = require("express");
const blogRouter = express.Router();
const passport = require("passport");
const {
    getAllBlogs,
    postBlog,
    updateBlogPost,
    deleteBlogPost,
    getBlog,
    getMyBlogs,
} = require("../controllers/blogs");
const blogValidator = require("../validators/blog.validator");

blogRouter.route("/").get(getAllBlogs);

blogRouter
    .route("/create")
    .post(
        passport.authenticate("jwt", { session: false }),
        blogValidator,
        postBlog
    );

blogRouter
    .route("/my_blogs")
    .get(passport.authenticate("jwt", { session: false }), getMyBlogs);

blogRouter
    .route("my_blogs/:id")
    .get(getBlog)
    .patch(
        passport.authenticate("jwt", { session: false }),
        blogValidator,
        updateBlogPost
    )
    .delete(passport.authenticate("jwt", { session: false }), deleteBlogPost);

module.exports = blogRouter;

Setting up a custom error handler was also paramount in this project as it allowed me to provide user-friendly error messages and provide different responses for different error types.

class Unauthorized extends Error {
  constructor(message) {
    super(message);
    this.statusCode = 401;
  }
}

module.exports = { Unauthorized };

To be extra safe, I also used an external error handler to handle errors that I might have missed in my app.js as well as some other packages for standard practice.

In my app.js, I also set up swagger-ui-express to the API. Swagger-ui-express is a package that generates interactive API documentation using the Swagger UI. It makes it easy for developers to test and understand the API, which is essential for maintaining and improving the API over time.

Everything tied together in the file and looked like this:

const express = require("express");
require("dotenv").config();
const app = express();
require("./middleware/authentication");
const errorHandler = require("./middleware/errorHandler");
require("express-async-errors");
const authRouter = require("./routes/auth");
const blogRouter = require("./routes/blogs");
const cors = require("cors");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const swaggerUi = require("swagger-ui-express");
const YAML = require("yamljs");

//swagger
const swaggerDocument = YAML.load("./swagger.yaml");

//middlewares
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(helmet());

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

//base routes
app.use("/auth", authRouter);
app.use("/blogs", blogRouter);

//error handler
app.use(errorHandler);

//swagger implementation
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));

//index page
app.get("/", (req, res) => {
    res.send('<h1>Blogs API</h1><a href="/api-docs">Documentation</a>');
});

app.use((req, res) => {
    res.status(404).json({ message: "page not found" });
});

module.exports = app;

I also added logging to the API using winston and morgan. Winston is a multi-transport logging library that allows developers to log messages to multiple sources, and morgan is a package that logs HTTP requests in a standard format. Together, these packages helped me keep track of what was happening in the API and debug any issues that arose.

Read more on logging in here

Finally, I added my test for all the routes that I had created to ensure that my application worked as expected and remains stable over time, even as changes are made to the code.

A sample code to show how the test setup looks for the auth routes:

const request = require("supertest");
const { connect } = require("./database");
const userModel = require("../models/User");
const app = require("../app");

describe("Auth: Signup", () => {
    let conn;

    beforeAll(async () => {
        // establish connection to database before running tests
        conn = await connect();
    });

    afterEach(async () => {
        // clean up code after every test
        await conn.cleanup();
    });

    afterAll(async () => {
        // close the database connection at the end of the tests
        await conn.disconnect();
    });

    it("should signup a user", async () => {
        //Send data as JSON
        const response = await request(app)
            .post("/auth/signup")
            .set("content-type", "application/json")
            .send({
                password: "secrett",
                first_name: "astro",
                last_name: "kashi",
                email: "kashi@gmail.com",
            });

        // Make sure the response is successful
        expect(response.status).toBe(200);
        // Make sure that response contains a message
        expect(response.body).toHaveProperty("message");
        // Make sure that user information is included in response body
        expect(response.body).toHaveProperty("user");
    });

    it("should login a user", async () => {
        //create user in db
        const user = await userModel.create({
            password: "secrett",
            first_name: "astro",
            last_name: "kashi",
            email: "kashi@gmail.com",
        });

        //attempt to login the user
        const response = await request(app)
            .post("/auth/login")
            .set("content-type", "application/json")
            .send({
                email: "kashi@gmail.com",
                password: "secrett",
            });

        // ensure the response was successful
        expect(response.status).toBe(200);
        // make sure response contains the expected message
        expect(response.body).toHaveProperty("message", "successfully logged in");
    });
});

The official Express documentation provides a section on testing, which includes information on how to write tests for your Express application. You can find the documentation at expressjs.com/en/guide/testing.html.


In conclusion, building a blog API using Express can be a fun and rewarding experience. With its powerful features and flexible architecture, Express makes it easy to create a RESTful API that serves your data consistently and efficiently. Whether you're building a personal blog or a large-scale content management system, Express has everything you need to get started. So, give it a try and see what you can build!