Skip to main content

Command Palette

Search for a command to run...

Build a Blogging REST API with Node.js: A simple Approach

If you're searching for a beginner-friendly approach to building a Blogging REST API with Node.js, you've come to the right place.

Published
34 min read
Build a Blogging REST API with Node.js: A simple Approach
S

I'm a Software Engineer specialized in Backend Engineering, having fluent knowledge in the Software Development Life Cycle and Test Driven Development. In terms of the Web framework, I have rich experience developing highly scalable Web APIs following the MVC Architecture and I have experience in both back-end and front-end development, developed many full-stack web based applications using Node.js, React.js, MongoDB & MySQL.

Introduction

To build a blogging API, you will need to:

  1. Decide on the functionality that you want your API to have. Some common features for a blogging API might include:
  • Creating, reading, updating, and deleting blog posts

  • Creating, reading, updating, and deleting comments

  • Reading a list of recent posts

  • Searching for posts by keyword or tag

  1. Choose a programming language and framework for building your API. Some popular choices for building APIs include Python with the Django or Flask framework or Node.js with the Express framework.

  2. Design the API endpoints and request/response formats. An API endpoint is a specific URL that serves a specific purpose, such as creating a new blog post or reading a list of recent posts. You will need to decide on the HTTP methods (such as GET, POST, PUT, DELETE) that each endpoint will support, as well as the format of the request and response data (such as JSON).

  3. Implement the API endpoints using your chosen programming language and framework. This will involve writing code to handle each endpoint's specific functionality, such as creating a new database record for a blog post or searching for posts by keyword.

  4. Test the API to ensure that it is working as expected. This might involve manually sending HTTP requests to the API endpoints and verifying that the correct responses are returned, or using a tool like Postman to automate the testing process.

  5. Document the API so that other developers know how to use it. This might include writing guidelines for how to make requests to the API, as well as examples of the request and response formats.

Alright now, let us dive right into the main show. In this article, we're going to be building an API for a blog. We will be covering a lot of concepts in this article. In case there are areas you are unclear about, I have included links to external resources throughout the article.

Description

The general idea is for a user to be able to do the following

  • Register an account.

  • Sign in to their account and get their API key

  • Using the API key, the user can access routes for managing blogs

  • Once logged in the user should be able to create, read, update, publish and delete blogs (commonly known as CRUD operations).

  • Anyone should be able to view published blogs

So these are our basic guidelines for the project. There might be more features but these are the core ideas behind the API.

Prerequisites

It is assumed that you have a basic knowledge of how servers work in general and that you are comfortable with using Express and MongoDB.

To follow along with this article, you'll need the following:

Building the project

Setting up the project

First things first let's create a folder to store all our program's files. I call mine "WeBlog-API" but you can use any name that's descriptive to you.

Once the folder is created, navigate to it in your terminal (cmd for windows or bash for Linux/macOS) and type the command npm init -y to initialize an npm package in the folder.

All the packages you'll need for this project are as follows:

  • express (Our HTTP server)

  • mongoose (For working with MongoDB)

  • dotenv (Loading environment variables from .env files)

  • passport, jsonwebtoken and passport-jwt (For handling authentication)

  • bcrypt (For hashing passwords stored in the database)

  • ejs (Template engine for express)

  • bootstrap (Basic styling)

  • cookie-parser (Parsing cookie headers from request)

  • validator (Basic regex validation)

Some other packages we'll be using for development and testing purposes are:

  • nodemon (Restarts our server whenever we make any changes to files)

  • jest (JavaScript testing framework)

  • supertest (For testing http servers)

  • mongodb-memory-server (For creating a temporary mongodb database for testing)

To install all normal dependencies, type the following in the terminal.

npm install bcrypt bootstrap cookie-parser dotenv ejs express jsonwebtoken mongoose passport passport-jwt validator

Development packages are simply packages that are not necessarily needed to run the app but can be useful during development. These packages should not be installed when deploying your application.

To install the development dependencies, type the following in the terminal.

npm install --save-dev nodemon jest supertest mongodb-memory-server

By the way, since nodemon and jest are heavy packages and you will likely use them a lot in your journey, it might be better to install them globally. This should not be done all the time though and this is just personal preference.

To install nodemon and jest globally instead, type the following in the terminal:

npm install -g nodemon jest

Running the project locally

To run this project locally, clone the GitHub repository below and read through the README file for directions on how to run it.

GitHub Repo: WeBlog-API

If you would like to view it now though and try it out for yourself, check out the live app:

Live App: WeBlog-API Live

Setting up the database

MongoDB Atlas is used for this project but you can also use the local installation if you wish. Create a new database and copy the URI link to it.

NOTE: You should never place passwords, secrets, connection strings or any other sensitive information in your files directly. Instead, make sure to use environment variables. More on environment variables here.

Create a new file and name it ".env" (make sure the dot is there) and place the URI string in it like this.

MONGODB_URI=<your connection string goes here>

Okay now create another file in the root directory and call it "db.js"

Here are the contents of the file:

// db.js

// Load the environment variables from the .env file in the root directory
require("dotenv").config()

// Load the mongoose library 
const mongoose = require("mongoose")
// Create a variable to hold the connection string to your database
const MONGODB_URI = process.env.MONGODB_URI

// Create a function to connect the app to your database. 
const connectDB =  function() {
    mongoose.connect(MONGODB_URI, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
      })

    mongoose.connection.on("connected", () => {
        // This is run when mongoose connects successfully to the database 
        console.log("Connected to MongoDB successfully")
    })
    mongoose.connection.on("error", (err) => {
        // This is run if there's any error connecting to the database
        console.log("Mongoose encountered an error")
        console.log(err)
    })
}

// Export the function
module.exports = connectDB

In the above code, we load the connection string from our ".env" file and pass that to mongoose to connect to the database.

Defining our data models

Next, we define the models for users and blogs.

Create a folder named "models" and in it create two files, "user.js" and "blog.js". These two files will contain all the attributes we care about regarding the object we're modeling.

The user model

Let's take a look at "user.js" first.

const { Schema, model } = require("mongoose")
const { isEmail } = require("validator").default
const bcrypt = require("bcrypt")

So at the top of our file, let's load all the modules we'll be needing. In the first line, we import Schema and model from the mongoose package. These two will help us in creating the model for the MongoDB document.

After that, we import the isEmail function from the validator module which is going to verify the email field against the standard email regex pattern.

Finally, we import bcrypt which we'll use to perform a hash on the password field of the document. It is important to note that you should NEVER store passwords in plaintext form in your database for security purposes. If you do and there's a security breach, your users' accounts will be compromised.

const userSchema = new Schema({
    first_name: {type: String, required: true},
    last_name: {type: String, required: true},
    full_name: String,

    email: {
        type: String,
        // Require the email to be unique accross the database
        unique: true,
        required: true,

        // Trim any extra whitespace at the beginning and end 
        trim: true,
        // Convert the email to lowercase before saving to the database
        lowercase: true,  

        // Ensure the email conforms to a standard pattern e.g. johndoe@example.com
        validate: {
            validator: isEmail,
            message: (props) => {
                // Message to be returned incase the validation fails
                return `${props.value} is not a valid email`;
            },
        }
    },

    password: {type: String, required: true}
})

Next, we define a schema to model the user. For this model, we only add basic fields for the user but you could add more as needed depending on the use case of your project. For instance, if you were modeling a user for a social media platform, you may add extra fields like follower_count and more bio-data (age, sex, nationality, e.t.c.).

userSchema.pre("save", async function(next) {
    // Set full name
    this.full_name = this.first_name + " " + this.last_name

    // Hash password
    const hash = await bcrypt.hash(this.password, 10)
    this.password = hash
    next()
})

After that, we attach something called middleware in mongoose. A middleware is a function that performs certain operations on data and passes the data on to the next middleware. The parameter, next, that we pass to the function represents the next middleware in the chain.

We use the save middleware which runs once before we save the data to the database. this references the actual document created and we can access all the fields using the dot notation.

We first set the full name and then we hash the password before saving it to the database.

userSchema.method("validatePassword", async function(password) {
    const isValid = await bcrypt.compare(password, this.password)
    return isValid
})

Next, we add an instance method to our schema. This method is attached to all documents created using this schema. It takes in a password as a parameter, hashes it and compares it to the original hashed password in the database to check if they're the same. It returns true if they are and false otherwise.

const User = model("User", userSchema)
module.exports = User

Finally, we create our mongoose model with the model function we imported earlier. The first argument passed is the string which will be used as the name of the collection in the actual MongoDB database while the second is the schema it will use to create the model.

We then export this model so we can use it in other modules.

All together our file should look like this:

// models/user.js

const { Schema, model } = require("mongoose")
const { isEmail } = require("validator").default
const bcrypt = require("bcrypt")

const userSchema = new Schema({
    first_name: {type: String, required: true},
    last_name: {type: String, required: true},
    full_name: String,

    email: {
        type: String,
        unique: true,
        required: true,

        trim: true,
        lowercase: true,

        validate: {
            validator: isEmail,
            message: (props) => {
                return `${props.value} is not a valid email`;
            },
        }
    },

    password: {type: String, required: true}
})

userSchema.pre("save", async function(next) {
    // Set full name
    this.full_name = this.first_name + " " + this.last_name

    // Hash password
    const hash = await bcrypt.hash(this.password, 10)
    this.password = hash
    next()
})

userSchema.method("validatePassword", async function(password) {
    const isValid = await bcrypt.compare(password, this.password)
    return isValid
})


const User = model("User", userSchema)
module.exports = User

The blog model

Next, we'll have a look at the model for our blog.

const { Schema, model } = require("mongoose")
const User = require("./user")

Same as before, we import Schema and model to use in defining the blog. We also import the User blog we just created as we'll be using it in the definition of the blog schema

const blogSchema = new Schema({
    // Title of the blog
    title: {  
        type: String,
        required: true,
        unique: true,
    },
    // Blog description
    description: String,
    // Author of the blog
    author: {
        type: Schema.Types.ObjectId,
        ref: User,  // The author is a reference to an existing user
        required: true
    },
    // State of blog indicating whether a blog is still a draft or is published
    state: {
        type: String,
        enum: ["draft", "published"],
        default: "draft"
    },
    // The number of views/reads the blog has had since it was published
    read_count: {
        type: Number,
        default: 0,
        min: 0,
    },
    // An approximation of the reading time of the blog
    reading_time: Number,
    // Tags/topics associated with the blog
    tags: [String],
    // The actual blog
    body: {
        type: String,
        required: true
    },
    // Date the blog was created
    timestamp: {
        type: Date,
        default: Date.now,
        immutable: true
    },
    // Date the blog was last updated
    last_modified: {
        type: Date,
        default: Date.now,
    }
})

One key thing to note in this schema is that the author field references an already existing user. It has a "type" of ObjectId which is a unique id that references a user. In the "ref" attribute of the author, we tell mongoose that the id value stored in this field is a reference to the user collection so that mongoose knows where to look when we ask to populate that field. If the "ref" wasn't placed there, mongoose would just treat this as a regular ObjectId field and not attach any other meaning to it.

// Create index on tags path 
blogSchema.index({ tags: 1 })

Additionally, we define an index on the "tags" field of the blog's schema.

An index in MongoDB is very similar to an index in real life (say an index at the back of a textbook). An index is used when you want to quickly find a reference to a particular word or phrase you are looking for.

I created this index because I felt like people would generally query blogs based on certain "tags" a lot, so it would be more efficient to create an index on it. Of course, you can add more indexes as you wish but be aware that they take up more space in the database.

const Blog = model("Blog", blogSchema)
module.exports = Blog

Finally, we also export this model to be used in other modules.

Altogether our blog model looks like this:

// models/blog.js

const { Schema, model } = require("mongoose")
const User = require("./user")

const blogSchema = 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"
    },
    read_count: {
        type: Number,
        default: 0,
        min: 0,
    },
    reading_time: Number,
    tags: [String],
    body: {
        type: String,
        required: true
    },
    timestamp: {
        type: Date,
        default: Date.now,
        immutable: true
    },
    last_modified: {
        type: Date,
        default: Date.now,
    }
})

// Create index on tags path 
blogSchema.index({ tags: 1 })


const Blog = model("Blog", blogSchema)
module.exports = Blog

Creating our routes

Okay, so now that our models have been defined. Let's begin to put them to use.

But before then let's break down a basic HTTP Request. A request typically consists of

  • HTTP Verb: These represent the action you want to perform for that request. The most common ones are.

    • GET: This is used whenever you request a website. It indicates that you want to view a resource.

    • POST: This is generally used when you submit a form to a server

    • PUT: This is used when you wish to update a resource on a server. e.g Update your profile information on Twitter.

    • DELETE: This is used when you wish to delete a resource. e.g Deleting a post on Facebook

  • Protocol: The part of the URL that indicates the rules for communication.

    Note that HTTPS represents a secure connection while HTTP does not. You should never submit personal information over an HTTP connection.

  • Domain: The name associated with the website where the page is loaded from

  • Routes: Also known as "paths" these are slash-separated words that define the path to a resource located on a web server.

HTTP Verb
    ▲         ┌─►Protocol            ┌───►Route/path
    │         │                      │
  ┌─┴─┐ ┌─────┴──┬───────────────┬───┴─────────────┐
  │GET│ │https://│www.example.com│/path/to/resource│
  └───┘ └────────┴──────┬────────┴─────────────────┘
                        │
               Domain ◄─┘

The 'auth' routes

So let's first take a look at the routes for authentication. We'll be having two routes, one for sign-up and the other for sign-in.

Create a folder named "routes" and inside the folder create a file named "auth.router.js". We add the router part to the filename to namespace it from other auth files.

Let's take a look at it.

const express = require("express")
const authRouter = express.Router()

First, let's import express and create a router.

A router is used to define routes and route handlers. The route is the string defining the URL path and the route handler is the function that is called when that route is requested. Each route must be tied to a particular HTTP Verb (although it's possible to define a catch-all route).

const { signupUser, signinUser }  = require("../controllers/auth.controller.js")

Next, we import the handlers (also known as controllers) that we'll be using for this group of routes.

The controller is a term that refers to the part of an application that handles processing and logic and generally communicates with the database when needed to fetch results.

// Handle the creation of a user when they submit the application form
// to the "/signup" route
authRouter.post("/signup", signupUser)

// Handle the necessary logic to login a user when they submit the 
// login form to the "/signup" route
authRouter.post("/signin", signinUser)

Next, we define two routes for authentication. One to create a new user and the other to log in the user.

module.exports = authRouter

Finally, we export the router.

The complete file should look like this:

// routes/auth.router.js

const express = require("express")
const authRouter = express.Router()

const { signupUser, signinUser }  = require("../controllers/auth.controller.js")


authRouter.post("/signup", signupUser)

authRouter.post("/signin", signinUser)


module.exports = authRouter

For this section, we need to add another environment variable to our ".env" file. Add a variable called JWT_SECRET. This secret will be used to sign the token we'll be creating later.

Your ".env" file should now look something like this:

MONGODB_URL=<your connection string goes here>
JWT_SECRET=<long and random sequence of characters>

Next, let's take a look at the handlers we used above.

Create a folder in the root directory of your app named "controllers". Inside the folder, add the file "auth.controller.js".

// Load the environment variables in the ".env" file 
require("dotenv").config()
// Import the `jwt` module we'll be using for authentication
const jwt = require("jsonwebtoken")
// Import the User model
const User = require("../models/user")

We'll be using the jsonwebtoken module to create and sign JWTs. We also need to import the User model as we'll be using it to create new users.

// Function to handle creation of user
async function signupUser(req, res) {
    // Extract submitted user information from the body of the request
    const userDetails = req.body
    const { first_name, last_name, email, password } = userDetails

    // Ensure that  all the necessary information was submitted 
    // before further processing
    if (!first_name || !last_name || !email || !password) {
        return res.status(400).send({
            message: "Please enter all the required info"
        })
    }

    // Create a new user with the given details
    const user = new User({ ...userDetails })

    // Catch any validation errors
    try {
        await user.validate()  // Validate user against schema definitions
    }
    catch (err) {
        return res.status(400).send(err) 
    }

    // Catch other errors that might occur with the database
    try {
        await user.save()
        // Return the user created
        return res.send({
            message: "User created successfully",
            user
        })

    } catch (err) {
        res.status(500).send(err)
    }

}

In this function, we first extract the fields from the form submitted through the request. The name of these fields will correspond to the name attribute on the input tag in the HTML form e.g. <input type="text" name="first_name">.

Next, we perform some checks and validations on the data and if they pass, we save the user to the database.


Now let's look at the next function

// Function to generate auth token for a user
async function signinUser(req, res) {
    // Extract email and password from the form submitted 
    const { email, password } = req.body
    // Ensure both email and password are included
    if (!email || !password) {
        return res.status(400).send({
            message: "Both email and password must be entered"
        })
    }
    // Search for user in the database by the email
    const user = await User.findOne({
        email
    }).exec()

  }

We again extract the necessary fields from the request body and ensure that they were all sent. Next, we search for the user in the database and save it to a variable

async function signinUser(req, res) {
    // ... previous code

    // Make sure the user with that email exists
    if (!user) {
        return res.status(404).send({
            message: "User with that email not found."
        })
    }
    // Validate the password
    const passwordIsValid = await user.validatePassword(password)
}

We then check to make sure the user was found and validate the password.

Notice the validatePassword method we use to check the password? This is the same one we defined on the user schema.

async function signinUser(req, res) {
    // ... previous code

    // If the password is valid, create user token
    if (passwordIsValid) {
        // Create the payload for the token
        const payload = { id: user._id }
        // Create token and sign it with secret
        const JWT_SECRET = process.env.JWT_SECRET
        const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" })

        // Try saving token to cookie for accessibility from a browser
        res.cookie("jwt", token, {httpOnly: true, maxAge: 60 * 60 * 1000})

        // Send token back as response to the user
        return res.send({
            message: "You've signed in successfully",
            token
        })

    // If the password is invalid return an error
    } else {
        return res.status(401).send({
            message: "Your password is incorrect! Try again"
        })
    }
}

If the password is valid, we move on to generate the token.

We call the jwt.sign method to sign our payload (which contains the id of the user) with our secret and set it to expire in one hour. In this app, I set the token to expire in one hour but you can change this to suit your needs.

Next, we save the token to a cookie. This will enable you to access some (not all) of the protected routes (only GET routes) using your browser.

module.exports = {
    signupUser,
    signinUser
}

Finally, we export the two handlers to be used in other modules.

The final file should look something like this:

// controllers/auth.controller.js

require("dotenv").config()

const jwt = require("jsonwebtoken")

const User = require("../models/user")

async function signupUser(req, res) {
    const userDetails = req.body
    const { first_name, last_name, email, password } = userDetails
    if (!first_name || !last_name || !email || !password) {
        return res.status(400).send({
            message: "Please enter all the required info"
        })
    }

    const user = new User({ ...userDetails })

    // Catch validation errors
    try {
        await user.validate()
    }
    catch (err) {
        return res.status(400).send(err)
    }

    // Catch other errors 
    try {
        await user.save()

        return res.send({
            message: "User created successfully",
            user
        })

    } catch (err) {
        res.status(500).send(err)
    }

}


async function signinUser(req, res) {
    const { email, password } = req.body

    if (!email || !password) {
        return res.status(400).send({
            message: "Both email and password must be entered"
        })
    }

    const user = await User.findOne({
        email
    }).exec()

    if (!user) {
        return res.status(404).send({
            message: "User with that email not found."
        })
    }
    const passwordIsValid = await user.validatePassword(password)

    if (passwordIsValid) {
        const payload = {
            id: user._id
        }
        // Create token and sign it with secret
        const JWT_SECRET = process.env.JWT_SECRET
        const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" })

        // Try saving token to cookie for accessibility from a browser
        res.cookie("jwt", token, {httpOnly: true, maxAge: 60 * 60 * 1000})

        return res.send({
            message: "You've signed in successfully",
            token
        })
    } else {
        return res.status(401).send({
            message: "Your password is incorrect! Try again"
        })
    }

}

module.exports = {
    signupUser,
    signinUser
}

The 'blog' routes

Next, we move to the blog routes. These are the routes that are going to be accessible by anyone and they don't require authentication.

Create another file inside the "routes" folder and name it "blog.router.js"

const express = require("express")
const blogRouter = express.Router()

So first, we import the express module and create our router.

const { getAllBlogs, getBlog } = require("../controllers/blog.controller")

// Get all published blogs
blogRouter.get("/", getAllBlogs)

// Get a specific blog 
blogRouter.get("/:id", getBlog)

Next, we import the controllers for this group of routes and define their paths. All these routes are going to be accessed through GET so we can easily view them from our browser.

You might notice the weird looking /:id path defined in the second route. This is a special syntax in express that allows us to define route parameters that capture the string in that position. It is a placeholder for any value and will be stored in the req.params object with a key of id.

module.exports = blogRouter

Finally, we export the router so we can use it elsewhere.

The whole module should look something like this now:

// routes/blog.router.js

const express = require("express")
const blogRouter = express.Router()

const { getAllBlogs, getBlog } = require("../controllers/blog.controller")

// Get all published blogs
blogRouter.get("/", getAllBlogs)

// Get a specific blog 
blogRouter.get("/:id", getBlog)


module.exports = blogRouter

Now let's take a look at those controllers we defined in our blog router file.

Create a file inside the "controllers" folder named "blog.controller.js"

const Blog = require("../models/blog")
const User = require("../models/user")

So the first thing we do is to import both models as we're going to be needing them for these functions.

// Function to get and return all published blogs
async function getAllBlogs(req, res, next) {
    // Initialize filter and sort queries
    const filterQuery = {}
    const sortQuery = {}

    // Filter/search params
    const author = req.query.author
    const title = req.query.title
    const tags = req.query.tags

    // Order/sorting params
    const read_count = req.query.read_count
    const reading_time = req.query.reading_time
    const timestamp = req.query.timestamp
}

Next, we define our function which we'll use to get all the blogs.

Now since most blogging platforms are expected to have hundreds or even thousands of blogs, it would be nice to be able to filter, sort and paginate these blogs so that the user isn't overwhelmed by the sheer amount of size of the results.

So, we define queries that the user can send with the request to filter and sort the blogs. I only defined three search and filter queries here but you can add more or less depending on your requirements.

// Function to get and return all published blogs
async function getAllBlogs(req, res, next) {
    // ... previous code

    // Handle possible filters
    title ? (filterQuery.title = { $regex: title, $options: "i" }) : null
    tags ? (filterQuery.tags = { $all: tags.split(",") }) : null
    if (author) {
        // Search for name of author
        const user = await User.findOne({
            // Search if the name query matches the author's full name
            full_name: { $regex: author, $options: "i" }
        }).exec()

        // If found, add id of author to filter query
        if (user) filterQuery.author = user._id
    }


    // Handle sort params
    const allowedSortValues = ["asc", "desc", "ascending", "descending", 1, -1]
    allowedSortValues.includes(read_count) ? (sortQuery.read_count = read_count) : null
    allowedSortValues.includes(reading_time) ? (sortQuery.reading_time = reading_time) : null
    allowedSortValues.includes(timestamp) ? (sortQuery.timestamp = timestamp) : null

}

Next, we use ternary operators to add the filter and sort queries to their respective objects if they exist.

// Function to get and return all published blogs
async function getAllBlogs(req, res, next) {
    // ... previous code

    try {
        const publishedBlogs = await Blog
            .find({ state: "published" })  // Only search for published blogs 
            .populate("author", "full_name email -_id")  // populate the author field
            .find(filterQuery)  // Apply the rest of the filters
            .sort(sortQuery)  // Apply the sort parameters
            .exec()

        // Handle pagination
        const pageSize = +req.query.pageSize || 20
        const page = +req.query.page || 1
        const start = (page - 1) * pageSize
        const end = page * pageSize

        const pagedBlogs = publishedBlogs.slice(start, end)

        res.send({
            message: "Successful!",
            matches: publishedBlogs.length,
            page,
            pageSize,
            blogs: pagedBlogs
        })

    } catch (err) {
        next(err)
    }

}

Finally, for this function, we apply the filter and sort queries to the search and define our pagination rules. The default page size is set to 20 and the default page is the first page.

In our search, we populate the author field from earlier with the document in the correct collection. MongoDB uses the ref field we defined on the author schema to populate it.

If everything works as expected we send the results back to the user; if not we pass an error to the registered handler.

// Get a single published blog by it's id
async function getBlog(req, res, next) {
    // Extract the "id" parameter from the request
    const id = req.params.id

    try {
        const blog = await Blog
            .findOne({ _id: id, state: "published" }) 
            .populate("author", "full_name email -_id")
            .exec()

        if (!blog) return res.status(404).send({ message: "Blog does not exist" })

        // Increment read count of blog by one
        blog.read_count++
        // Save the changes to the database
        await blog.save()

        // Send the blog back
        res.send(blog)

    } catch (err) {
        next(err)
    }
}

For the second function, we search the database for a published blog with the given id.

If we find the result we increment the read count (number of views) by one and then return the results. Else, if there's an error, we pass it on to the error handler.

module.exports = {
    getAllBlogs,
    getBlog
}

Finally, we export the functions we created.

The file should now look something like this:

// controllers/blog.controller.js

const Blog = require("../models/blog")
const User = require("../models/user")


async function getAllBlogs(req, res, next) {
    const filterQuery = {}
    const sortQuery = {}

    // Filter/search params
    const author = req.query.author
    const title = req.query.title
    const tags = req.query.tags

    // Order/sorting params
    const read_count = req.query.read_count
    const reading_time = req.query.reading_time
    const timestamp = req.query.timestamp

    // Handle possible filters
    title ? (filterQuery.title = { $regex: title, $options: "i" }) : null
    tags ? (filterQuery.tags = { $all: tags.split(",") }) : null
    if (author) {
        // Search for name of author
        const user = await User.findOne({
            full_name: { $regex: author, $options: "i" }
        }).exec()

        // If found, add id of author to filter query
        if (user) filterQuery.author = user._id
    }


    // Handle sort params
    const allowedSortValues = ["asc", "desc", "ascending", "descending", 1, -1]
    allowedSortValues.includes(read_count) ? (sortQuery.read_count = read_count) : null
    allowedSortValues.includes(reading_time) ? (sortQuery.reading_time = reading_time) : null
    allowedSortValues.includes(timestamp) ? (sortQuery.timestamp = timestamp) : null


    try {
        const publishedBlogs = await Blog
            .find({ state: "published" })
            .populate("author", "full_name email -_id")
            .find(filterQuery)
            .sort(sortQuery)
            .exec()

        // Handle pagination
        const pageSize = +req.query.pageSize || 20
        const page = +req.query.page || 1
        const start = (page - 1) * pageSize
        const end = page * pageSize

        const pagedBlogs = publishedBlogs.slice(start, end)

        res.send({
            message: "Successful!",
            matches: publishedBlogs.length,
            page,
            pageSize,
            blogs: pagedBlogs
        })

    } catch (err) {
        next(err)
    }

}

async function getBlog(req, res, next) {
    const id = req.params.id

    try {
        const blog = await Blog
            .findOne({ _id: id, state: "published" })
            .populate("author", "full_name email -_id")
            .exec()

        if (!blog) return res.status(404).send({ message: "Blog does not exist" })

        // Increment read count of blog by one
        blog.read_count++
        await blog.save()

        res.send(blog)

    } catch (err) {
        next(err)
    }
}

module.exports = {
    getAllBlogs,
    getBlog
}

The 'writer' routes

Next, we'll look at the writer routes which will be the ones that require authentication. These are only going to be accessible to users with a valid token.

Create a file named "writer.router.js" in the "routes" folder.

Here are the contents of the file:

// routes/writer.router.js

const express = require("express")
const writerRouter = express.Router()

const { 
        createBlog, 
        getUserBlog, 
        editBlog, 
        deleteBlog, 
        publishBlog 
} = require("../controllers/writer.controller")

// Create a new blog
writerRouter.post("/blog", createBlog)

// Get all blogs by the logged in user
writerRouter.get("/blog", getUserBlog)

// Publish a blog
writerRouter.patch("/publish/:id", publishBlog)

// Edit a blog
writerRouter.patch("/edit/:id", editBlog)

// Delete a blog
writerRouter.delete("/blog/:id", deleteBlog)

module.exports = writerRouter

Most of this code is similar to the ones for the "auth" and "blog" routes. The major differences are the patch and delete methods we use here. These are used to update a resource and delete a resource from the server respectively.


Let's now analyze the controllers that perform these functions.

Create a file named "writer.controller.js" in the "controller" folder.

const Blog = require("../models/blog")

async function createBlog(req, res, next) {
    const blogDetails = req.body

    // Get logged in user's id
    const userId = req.user.id

    try {
        /**
         * Calculate the reading time (in minutes) of the 
         * blog based on the length of the body.
         * 
         * Based on research the avg reading time is
         * about 200 words per minute.
         */
        const wordCount = blogDetails.body.split(/\s+/).length
        const reading_time = Math.floor(wordCount / 200) || 1

        // Split the tags on spaces or commas
        const tags = blogDetails.tags.split(/\s+|\s*,\s*/)

        // Create the blog.
        const blog = new Blog({
            ...blogDetails,
            author: userId,
            reading_time,
            tags
        })

        await blog.save()

        res.status(201).send({
            message: "Blog created successfully!",
            blog
        })

    } catch (err) {
        next(err)
    }
}

Here we extract the blog details from the form and calculate the average reading time of the blog based on the fact that most people read about 200 words per minute.

We create the blog and save it to the database and send the result back to the user.

async function getUserBlog(req, res, next) {
    // Get logged in user's id
    const userId = req.user.id

    const query = req.query

    try {
        const filterQuery = {}

        // Filter by state
        const state = query.state
        state ? (filterQuery.state = state) : null

        const writerBlogs = await Blog.find({
            author: userId,
            ...filterQuery
        }).exec()

        // Handle pagination
        const pageSize = +query.pageSize || 20
        const page = +query.page || 1
        const start = (page - 1) * pageSize
        const end = page * pageSize

        const pagedBlogs = writerBlogs.slice(start, end)

        res.send({
            message: "Successful!",
            total: writerBlogs.length,
            page,
            pageSize,
            blogs: pagedBlogs
        })

    } catch (err) {
        next(err)
    }

}

Next, we define the function to get all the author's blogs. This function is very similar to the one we wrote for the "blog" routes with the key difference that only blogs written by this particular author are returned.

async function publishBlog(req, res, next) {
    const blogId = req.params.id

    try {
        const blog = await Blog.findById(blogId).exec()

        if (!blog) {
            return res.status(404).send({
                message: "Blog not found!"
            })
        }

        if (blog.state === "published") {
            return res.send({
                message: "This blog has already been published!"
            })
        }

        blog.state = "published"

        await blog.save()

        res.send({
            message: "Your blog has been published!",
            blog
        })
    } catch (err) {
        next(err)
    }
}

The next controller we define is to change the state of a certain blog to published. This will perform some basic checks and if all passes, it will publish the blog and return the result to the user.


async function editBlog(req, res, next) {
    const blogId = req.params.id

    // Get possible fields to be updated
    const title = req.body.title
    const description = req.body.description
    const tags = req.body.tags
    const body = req.body.body

    try {
        const blog = await Blog.findById(blogId).exec()

        if (!blog) {
            return res.status(404).send({
                message: "Blog not found!"
            })
        }

        // Update blog based on values
        title ? (blog.title = title) : null
        description ? (blog.description = description) : null
        tags ? (blog.tags = tags.split(/\s+|\s*,\s*/)) : null
        body ? (blog.body = body) : null

        // Calculate new reading_time of blog
        const wordCount = body.split(/\s+/).length
        const reading_time = Math.floor(wordCount / 200) || 1
        blog.reading_time = reading_time

        // Update last_modified to current time
        blog.last_modified = Date.now()


        await blog.save()

        return res.send({
            message: "Blog updated successfully!",
            blog
        })

    } catch (err) {
        next(err)
    }
}

Next, we define the controller to update the blog.

This

  • extracts the fields to be updated

  • recalculates the reading time

  • updates the modified date of the blog

  • and if all goes well, saves the blog to the database


async function deleteBlog(req, res, next) {
    const blogId = req.params.id

    try {
        const blog = await Blog.findById(blogId).exec()

        if (!blog) {
            return res.status(404).send({
                message: "Blog not found!"
            })
        }

        // Delete the blog
        await Blog.deleteOne({ _id: blogId })

        res.send({
            message: "Blog deleted successfully!",
            blog
        })

    } catch (err) {
        next(err)
    }
}

module.exports = {
    createBlog,
    getUserBlog,
    editBlog,
    deleteBlog,
    publishBlog
}

The final controller is to delete the blog from the database. Once the blog is deleted we send confirmation to the user.

Afterward, we export all controllers to be used in other files.

The final file should look like this:


Handling authentication and authorization

Authentication is the process of verifying whether a user is who they say they are while authorization is the process of determining whether our user has the required privileges to access a particular resource.

We'll be using passport to handle authorization in our app. Passport is a very popular authentication handler that supports the use of different "strategies". These strategies include social auth (Google, Facebook, GitHub...), local auth (username and password), tokens (Json Web Tokens, ...) and so much more.

JWTs are very popular for use of authentication in APIs hence in this project we'll be using Json Web Tokens and therefore the passport-jwt strategy.

Create a file named "auth.js" and place it in the root directory of your project.

const passport = require("passport")
const User = require("./models/user")

First, we import the passport module and the User model we defined earlier. We'll be using the User model to validate the id of the user who wants to access a given route. This is the process of authentication we talked about earlier.

const JWTStrategy = require("passport-jwt").Strategy
const {
    fromAuthHeaderAsBearerToken, fromExtractors
} = require("passport-jwt").ExtractJwt


// Cookie extractor function
function fromCookie(req) {
    let token = null
    if (req.cookies) token = req.cookies["jwt"]
    return token
}

We import the JWT strategy we'll be using. We also import two functions; one will be used to extract a token from the Authorization header in an HTTP request and the other will allow us to use multiple extractors, testing each one until one of them returns a value other than null.

We define a custom extractor that will search for the token in the cookies.

passport.use(new JWTStrategy(
    {
        // Secret used to verify the token is valid
        secretOrKey: process.env.JWT_SECRET,
        jwtFromRequest: fromExtractors([
            // Check for token in "Auth" header first
            fromAuthHeaderAsBearerToken(),
            // Check for token in browser cookies
            fromCookie
        ])
    },
    async (payload, done) => {
        // Retrieve the user's id from the payload
        let userId =  payload.id
        if (!userId) {
            const err = new Error("UserId is not included in token")
            err.status = 400
            return done(err)
        }
        const user = await User.findById(userId).exec()
        if (!user) {
            const err = new Error("User does not exist (maybe was deleted)")
            err.status = 404
            return done(err)
        }
        done(null, {id: userId})
    }
))

We create a new instance from the JWTStrategy class and then pass that to the use function to configure passport.

We pass two arguments to the strategy; the first is the options object that takes the required secret and extractor functions; the next is the callback function to verify the user.

// Middleware we'll use to authenticate routes.
function authenticate(req, res, next) {
    passport.authenticate(
        "jwt", 
        {session: false}, 
        (err, user, info, status) => {
            // Return errors from jwt strategy
            if (err) return next(err)

            // Return other errors
            if (info) return next(info)

            // Attach the user to the request object
            req.user = user

            // Continue on to the next middleware
            next()
        }
    )(req, res, next)
}

module.exports = authenticate

Finally, we define the middleware that we'll be using to protect routes. This middleware in turn uses passport's authenticate function to authenticate the route using the JWT schema.

The reason we have to specify which strategy to use by passing "jwt" as the first argument to authenticate is that you can register multiple strategies with passport.

We also tell passport not to initialize a session because tokens must be sent on every request and they are not stored server-side.


Defining the views

I defined three views for this project although they are not strictly needed. I used the ejs template engine to define the templates we'll use.

I won't go into much detail here but I'll just share the files and you can inspect them. I used bootstrap to provide basic styling.

Create a new folder named "views" and inside create two files; "signin.ejs" and "signup.ejs"

<!-- views/signin.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <%- include("./partials/head")  %>
    <title>Signin</title>
</head>
<body>
    <%- include("./partials/nav") %>

    <div class="container">
        <h2>Signin</h2>
        <p>Login to get your API token</p>
        <form action="auth/signin" method="post">
            <div class="mb-3">
                <label class="form-label" for="email">Email</label>
                <input class="form-control" type="email" name="email" id="email" required>
            </div>

            <div class="mb-3">
                <label class="form-label" for="pass">Password</label>
                <input class="form-control" type="password" name="password" id="pass" required>
            </div>
            <input type="submit" class="btn btn-primary" value="Login">
        </form>
    </div>
</body>
</html>
<!-- views/signup.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <%- include("./partials/head")  %>
    <title>Signup</title>
</head>
<body>
    <%- include("./partials/nav") %>

    <div class="container">
        <h2>Signup</h2>
        <p>Create an account to use the blogging API</p>
        <form action="/auth/signup" method="post">
            <div class="mb-3">
                <label class="form-label" for="fn" >First name</label>
                <input class="form-control" type="text" name="first_name" id="fn" required>
            </div>

            <div class="mb-3">
                <label class="form-label" for="ln">Last name</label>
                <input class="form-control" type="text" name="last_name" id="ln" required>
            </div>

            <div class="mb-3">
                <label class="form-label" for="email">Email</label>
                <input class="form-control" type="email" name="email" id="email" required>
            </div>

            <div class="mb-3">
                <label class="form-label" for="pass">Password</label>
                <input class="form-control" type="password" name="password" id="pass" required>
            </div>

            <input type="submit" class="btn btn-primary" value="Signup">
        </form>
    </div>
</body>
</html>

I moved the partial files to a sub-folder called partials. They are "head.ejs" and "nav.ejs"

<!-- views/partials/head.ejs -->

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Icon -->
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
    <link rel="manifest" href="/site.webmanifest">
    <!-- Bootstrap -->
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <script src="js/bootstrap.min.js"></script>
</head>
<!-- views/partials/nav.ejs -->

<nav class="navbar navbar-expand-lg bg-light mb-5">
    <div class="container-fluid container">
        <h1 class="navbar-brand"><a href="/" class="nav-link">Blogging API</a></h1>
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item"><a href="/signup" class="nav-link">Signup</a></li>
            <li class="nav-item"><a href="/signin" class="nav-link">Signin</a></li>
        </ul>
    </div>
</nav>

Bringing everything together

Now, let's create our app.js file which will define all our routes.

In the root directory create two files: one named "app.js" and the other "index.js"

Let's first take a look at the "app.js" file

require("dotenv").config()
const express = require("express")
const cookieParser = require("cookie-parser")

// Import all the routers
const authRouter = require("./routes/auth.router")
const blogRouter = require("./routes/blog.router")
const writerRouter = require("./routes/writer.router")

// Require the authentication middleware
const authenticate = require("./auth")

First, we load all the environment variables. Then we import express and cookie-parser package we'll be using.

We also import all the routes we've defined so far and finally import the authenticate middleware we'll be using to validate the "writer" routes.

// Create express app 
const app = express()

// Set view engine and tell express where the views are
app.set("view engine", "ejs")
app.set("views", "./views")

// Add middleware to process form submissions
app.use(express.urlencoded({extended: false}))
// Add middleware to serve static files
app.use(express.static(__dirname + "/public"))
app.use(express.static(__dirname + "/node_modules/bootstrap/dist"))
// Add middleware to parse cookies from the headers
app.use(cookieParser())

We create our express app, define some settings for our views and attach some application-level middleware.

// EJS Views
app.get("/", (req, res) => {
    res.render("home")
})
app.get("/signup", (req, res) => {
    res.render("signup")
})
app.get("/signin", (req, res) => {
    res.render("signin")
})

// API Routes
app.use("/auth", authRouter)
app.use("/blog", blogRouter)
app.use(
    "/writer",
    authenticate,  // Make sure all requests are authenticated
    writerRouter
)

Now we attach the views we defined to the app and also attach the routes to certain prefixes in the app.

// Handle unknown routes
app.use((req, res, next) => {
    res.status(404).send({
        message: "Sorry, the route you requested does not exist!"
    })
    next()
})

// Error handler
app.use((err, req, res, next) => {
    res.status(err.status || 500).send({
        message: "An error occured. Oops!",
        error: err.toString()
    })
})

module.exports = app

Finally, we define a middleware to handle any routes we didn't define by sending an error 404 message. If we didn't do this, our app would crash every single time a user made a typo in the route.

We also define an express error handler middleware that will process errors we send to it. Throughout the project there were times I called next(err); what this was doing was sending the errors to this middleware.


The last file we'll look at for this project is "index.js". This is the file that is going to be the entry point to the app and where we are going to run our server from.

Create a file named "index.js" in the root directory of the project.

// index.js

// Load environment variables 
require("dotenv").config()

const app = require("./app")
const connectDB = require("./db")

// Get port from environment or use 3000 as a default
const port = process.env.PORT || 3000

// Start application
app.listen(port, () => {
    console.log("App is listening on http://localhost:" + port)
})

// Connect to MongoDB instance
connectDB()

Here we import our express app and the function to connect to our database.

Next, we get the port our app should run on and start the express server on that port.

Finally, we connect to our database and were done!

Conclusion

So that's it guys. I hope we're able to follow along and that you enjoyed this article.

There were a lot of concepts covered here so I understand if you didn't understand everything the first time. If you have any questions you can reach me through the comments section below and I'll be happy to respond.

If you liked this article, consider following me on Hashnode for my latest publications. I tweet my journey on Twitter daily, share weekly content on my LinkedIn, constantly build and collaborate on projects on GitHub and this is my IG :)

Meanwhile, you can help yourself (and help me) by checking out my Youtube channel. I post great content there and have even better videos coming up!

Cheers!