How to build a simple Blog REST API using Nodejs, Expressjs, JSON Web Token(JWT), and MongoDB.

A beginner- friendly walk-through on how to build a simple blog REST API from scratch.

·

27 min read

How to build a simple Blog REST API  using Nodejs, Expressjs, JSON Web Token(JWT), and MongoDB.

In this tutorial, we will walk through the steps of creating a simple, server-side blog API that allows users to create, read, update, and delete blog articles using Model-View-Controller (MVC) architecture.

As a prerequisite, You should have considerable knowledge of basic and asynchronous Javascript, express framework, and MongoDB setup.

Before we jump right in, let's take a look at what the term "REST API" means.

What is a REST API?

An application Programming Interface (API) is a set of rules and protocols that allows different computer systems to talk to each other and share information. It's like a unique set of instructions that specifies how one system can ask another system to do something or to send it some data.

Representational State Transfer (REST) is a popular architecture for building APIs that use HTTP as the communication protocol. You can read more about it here.

In the context of a blog, a blog API might allow other applications or websites to retrieve data from the blog, such as a list of articles or the content of a specific article.

Project Requirements.

Below are the functionalities we would be implementing on our blog API.

1. Define the User model and Blog model.
2. A user should be able to sign up and sign in into the blog app
3. Use JWT as authentication strategy and expire the token after 1 hour
4. A blog can be in two states; draft and published
5. Logged in and not logged in users should be able to get a list of published blogs created
6. Logged in and not logged in users should be able to to get a published blog
7. Logged in users should be able to create a blog.
8. When a blog is created, it is in draft state
9. The owner of the blog should be able to perform CRUD (Create Read Update Delete) on either draft or published state.
13. The endpoint should be paginated and filterable by state
14. When a single blog is requested, the api should return the user information(the author) with the blog. The read_count of the blog too should be updated by 1.

Getting Started.

To get started, you will need to have Nodejs, Node package manager (npm) and MongoDB installed on your system.

If you don't already have them installed, you can download and install nodejs and npm from the Nodejs website and MongoDb from the MongoDB website.

Project Setup.

Once you have Node.js, npm, and MongoDB installed, create a new directory for your project and open a terminal window in that directory.

Then, run the following command on your terminal to create a package.json file, which will store the metadata for your project:

npm init -y

This is how your package.json file would look like when you open it.

{
  "name": "blog_api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Starting the server

  • Install express by running the command: npm install express. This automatically creates a node_modules and package-lock.json file.

  • Create a .env file and define the server port number: PORT=4000

The .env file is a configuration file that is used to set environment variables for a Node.js application, and they are often used to store sensitive information such as database passwords or API keys that should not be hardcoded into the application's source code.

  • Create a .gitignore file and save the .env file and node_modules in it.

This is because we want to exclude them from version control (e.g. Git) to prevent sensitive information from being committed and shared publicly.

.env
node_modules

The .gitignore is a configuration file that is used to tell Git, a version control system, which files or directories to ignore when committing changes to a repository.

  • Install dotenv library by running the command: npm install dotenv.

This library allows you to load the environment variables from the .env file into the process.env object which would then allow you access to these environment variables on your application by requiring the library in this format: require('dotenv').config();

  • Create an app.js file, require express and dotenv and then run the following command to start the server:
const express = require("express");
require("dotenv").config();

const app = express();
const port = process.env.PORT;

app.use(express.json());

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

The app.use(express.json()) is a middleware function registered with the Express app, and it will parse any JSON request bodies sent to the server. Another alternative is to install body-parser.

  • Run the app with the following command: node app.js
ikemv@Violacordis MINGW64 ~/blog_API
$ node app.js
Server is running on port 4000

Now, our server is running on port 4000.

Defining the homepage route.

This route is a single route that responds to a GET request to the root URL of the app. When this route is called, it sends a "Welcome to my Blog Website" message back to the client.

app.get("/api/v1", (req, res) => {
  return res.status(200).json({
    status: "success",
    message: "Welcome to my Blog Website",
  });
});

Catching all undefined routes.

In an Express.js application, you can use a catch-all route to handle requests for undefined routes. A catch-all route is a route that is defined with a wildcard path (*) and is used to match all requests that do not match any other routes in the application.

// Catching all undefined routes
app.all('*', (req, res, next) => {
  res.status(404).json("Page not found");
});

Here, the catch-all route is defined using the app.all() method and a wildcard path. It will match any request that does not match any other routes in the application and will send a "Page not found" message to the client with a status code of 404 (Not Found).

Defining our Error handling middleware.

Error handling middleware is a type of middleware in a Node.js application that is designed to handle errors that occur during the processing of an HTTP request.

They are typically placed at the end of the middleware chain, after all other middleware functions and route handlers, so that they can catch any errors that were not handled by other middleware.

// Error handling middleware

app.use((err, req, res, next) => { 
console.error(err); 
res.status(500).send('Something went wrong!');
 });

Our app.js file

const express = require("express");

require("dotenv").config();

const app = express();
const port = process.env.PORT;

app.use(express.json());

// Initial route => Homepage
app.get("/api/v1", (req, res) => {
  return res.status(200).json({
    status: "success",
    message: "Welcome to my Blog Website",
  });
});

// Catching all undefined error
app.all("*", (req, res, next) => {
  res.status(404).json("Page not found");
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send("Something went wrong!");
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

Defining the Models: The user and blog models.

In an MVC application, the model is typically the part of the application that handles the data and the business logic. It is responsible for storing and manipulating data, such as information about users or products, and for performing operations on that data, such as retrieving, updating, or deleting it.

To define the user model:

  • Create a userModel.js file in a models folder.

  • Install and require the mongoose library on the file.

  • Define the user schema and define the following properties in it: firstName, lastName, email and password.

  • Define a relationship between users and their articles such that when an article is requested, the API should display the article together with the user's (author) information.

This is where mongoose referencing & population come in, where the ref is used to link a model to another.

In your user model, add article property and ref the blog model using its Index just like in the code below.

  • Define the model in this format: const User = mongoose.model("User", userSchema); where User and userSchema are the model and schema variables respectively.

  • Export the model: module.exports = User;

  • Check out the mongoose documentation for more guidance on how to define your models.

// User Model
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const validator = require("validator");

const userSchema = new Schema(
  {
    firstName: {
      type: String,
      required: [true, "Please provide your first name"],
    },
    lastName: {
      type: String,
      required: [true, "Please provide your last name"],
    },
    email: {
      type: String,
      required: [true, "Please provide your email"],
      unique: [true, "This email already exists"],
      lowercase: true,
      validate: [validator.isEmail, "Please provide a valid email"],
    },
    password: {
      type: String,
      required: [true, "Please provide a password"],
      minlength: [5, "Password must be at least 5 characters"],
      select: false,
    },
    articles: [
      {
        type: Schema.Types.ObjectId,
        ref: "Blog",
      },
    ],
  },
  { timestamps: true }
);

const User = mongoose.model("User", userSchema);
module.exports = User;
  • The User model has several fields such as firstName, lastName, email, password, and articles.

  • The 'required' property for each field specifies that these fields are required when creating a new user.

  • The 'unique' property for the email field specifies that the email must be unique across all users.

  • The 'validate' property for the email field specifies that the email must be a valid email address.

  • The 'minlength' property for the password field specifies that the password must have at least 5 characters.

  • The 'select: false' property for the password field specifies that the password should not be returned when querying the database for a user.

  • The 'timestamps' property adds createdAt and updatedAt fields to the model which store the time when a user is created and updated.

  • Finally, the model is exported to be used in other parts of the application.

Now that we are done with the user model, you can use the same approach on your blog model. In your blog model, add author property and ref the User model using its Index just like in the code below.

// Blog model
const mongoose = require("mongoose");
const User = require("./userModel");
const Schema = mongoose.Schema;

const blogSchema = new Schema(
  {
    title: {
      type: String,
      required: [true, "Please provide the title"],
      unique: [true, "The title name already exists"],
    },
    description: {
      type: String,
    },
    author: {
      type: String,
      required: [true, "Please provide the author"],
    },
    state: {
      type: String,
      enum: ["draft", "published"],
      default: "draft",
    },
    read_count: {
      type: Number,
      default: 0,
    },
    reading_time: {
      type: String,
      required: [true, "Please provide the reading time"],
    },
    tags: {
      type: [String],
      required: [true, "Please provide the tags"],
    },
    body: {
      type: String,
      required: [true, "Please provide the body"],
    },
    user: {
      type: Schema.Types.ObjectId,
      ref: "User",
    },
  },
  { timestamps: true }
);

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

Connecting to MongoDB

It's time to connect our application to a database (MongoDB), and to do that:

  • First, copy your MongoDB connection string or URL. Refer to this documentation if you don't know how to do that.

  • Define it in your .env file: MONGODB_CONNECTION_URL=mongodb+srv://(username):(insert Password)@cluster0.33jojrj.mongodb.net/Blog_API

  • Create a db_connect.js file and require dotenv library so as to access your .env.

    Define a function that connects your application to your database using the database URL and then export it.

const mongoose = require("mongoose");
require("dotenv").config();

const MONGODB_CONNECTION_STRING = process.env.MONGODB_CONNECTION_STRING;

const connectToMongoDB = () => {
  mongoose.connect(MONGODB_CONNECTION_STRING);

  mongoose.connection.on("connected", () => {
    console.log("Successfully Connected to MongoDB");
  });

  mongoose.connection.on("error", (err) => {
    console.log(`Error connecting to MongoDB`, err);
  });
};

module.exports = { connectToMongoDB };
  • Import the file to your app.js and call the function just after starting your express application: connectToMongoDB();

Run your application to check if the connection was successful.

ikemv@Viola MINGW64 ~/blog_API
Server is running on port 4000
Successfully Connected to MongoDB

Now, we have successfully connected our application to the database.

Performing CRUD functionalities.

CRUD, an acronym that stands for Create, Read, Update, and Delete, refers to the basic operations that can be performed on data in a database or other persistent storage.

The CRUD operations are as follows:

  • Create: Adding new data to the database or storage system.

  • Read: Retrieving existing data from the database or storage system.

  • Update: Modifying existing data in the database or storage system.

  • Delete: Removing data from the database or storage system.

To implement CRUD functionality on your blog API, you will need to define a set of routes that handle HTTP requests to create, read, update, and delete blog articles.

Routes and Controllers (Request handlers).

In a Node.js application using the Express framework, routes and controllers are used to handle HTTP requests and responses.

Routes are responsible for specifying the path and the HTTP method (such as GET, POST, PUT, DELETE) for a request, and mapping the request to the appropriate controller function.

Controllers are functions that are defined to handle incoming requests to specific routes. These functions are usually responsible for performing some logic or interacting with a database, and then sending a response back to the client.

Defining Blog API Routes.

In this Blog API, we are going to have the following routes:

// user authentication 
POST api/v1/auth/signup - For user signup.
POST api/v1/auth/login - For user login.

// Public routes
GET api/v1/ - Homepage
GET api/v1/blog - For getting all published articles.
GET api/v1/blog/:id - For getting a specific published article.

// Private routes => Protected routes
POST api/v1/blog/articles - For creating an article.
GET api/v1/blog/articles/:id - For getting all user articles(draft & published). 
PUT api/v1/blog/articles/:id - For updating a specific user article.
DELETE api/v1/blog/articles/:id - For deleting a specific user article.

NB => Using versioning (just like I did and represented with v1 meaning version 1) is one of the good practices when designing a REST API, as it ensures that your API can evolve over time without breaking existing clients.

Now, let's define the routes on the app.js after database connection like this:

// Defining the routes 
app.post("api/v1/auth/signup", write controller function name here); 
app.post("api/v1/auth/login", write controller function name here); 

// Public routes
app.get("api/v1/blog", write controller function name here); 
app.get("api/v1/blog/:id", write controller function name here); 

// Private routes => Protected routes
app.post("api/v1/blog/articles", write controller function name here);  
app.get("api/v1/blog/articles/:id", write controller function name here);  
app.put("api/v1/blog/articles/:id", write controller function name here); 
app.delete("api/v1/blog/articles/:id", write controller function name here);

This code defines a set of routes for our blog API. Each route is defined using the app.METHOD() function, where METHOD is the HTTP method (such as GET, POST, PUT, or DELETE) and the path is the URL of the route.

We have not yet defined our controller functions, so we will be using write controller function name here as the controller function name for now till we implement the CRUD functionalities.

The routes are divided into three categories:

  • api/v1/auth/signup and api/v1/auth/login: These routes are used for user signup and login, respectively. They are implemented using the write controller function name here controller functions.

  • api/v1/blog and api/v1/blog/:id: These routes are public and can be accessed by anyone. They are used to retrieve a list of all blogs or a specific blog by ID, respectively. They are implemented using the write controller function name controller functions.

  • api/v1/blog/articles/:id, api/v1/blog/articles/:id, api/v1/blog/articles/:id, and api/v1/blog/articles/:id: These routes are private and protected, meaning that they can only be accessed by authenticated users. They are used to create, retrieve, update, and delete articles, respectively. They are implemented using the write controller function name controller functions.

Defining the Blog API Controller functions.

Controller functions are typically defined in a separate file (often called a "controller file") from the main application file. This helps to separate the logic for handling requests from the main application logic and makes it easier to organize and maintain the codebase.

We will be creating two controller files, one for user management authController.js and the other for blog management blogController.js.

Let's start with the user management controller functions.

User management.

User management refers to the set of features and functions that enable users to create accounts, sign in, sign out, and update their account information.

We are going to use JSON web token to authenticate users during the signup and login process, then set an expiry time option which logs a user o

Authentication with JSON Web Token (JWT)

Authentication is the process of verifying the identity of a user or system. It is a common security measure that is used to ensure that only authorized users or systems are able to access certain resources or perform certain actions.

JWT (JSON Web Token) is commonly used as a way to authenticate users and transmit information about the user between systems, such as between a client and a server or between two servers. it acts as a token that represents the user's identity and can be used to authenticate the user on subsequent requests.

JWT Signing

JWT signing takes place during the user signup and login process after the user's credentials have been verified and the user has been authenticated.

JWT is signed using the sign function from the JSON web token library, which takes the payload (the information to be included in the JWT), the secret, and the options as arguments.

The signature is used to verify the authenticity of the JWT and ensure that it has not been tampered with.

Now, let's define our JWT signing function using the user ID as the payload, secret key, and expiry time, and we are going to call this function during the user signup and login process.

  • First, define your secret key and expiry time in your .env file:
JWT_SECRET=writeYourSecretKeyHere
JWT_EXPIRES_IN=1h
  • Create a auth.js file and import the .env file.

  • Then, define the JWT signing function using the user ID as the payload like the code below:

 const signToken = (id) => {
  return jwt.sign({ id }, JWT_SECRET, {
    expiresIn: JWT_EXPIRES_IN,
  });
};
  • Call the function on the signup and login process after the user credentials are verified and the user is authenticated like this : const token = signToken(newUser._id);

Signup Functionality.

The following is what happens during user signup:

  • The user provides their credentials (first name, last name, email, and password) to the server.

  • The server verifies the credentials and, if they are valid, creates a JWT that contains information about the user.

  • The server signs the JWT using the provided secret Key.

  • The server hashes the user's password using a secure hashing function such as bcrypt.

  • The server stores the hashed password in the database (along with the user's credentials).

  • The server sends the signed JWT to the client (typically in the form of an HTTP cookie or an HTTP header).

  • The client stores the JWT and sends it back to the server with each subsequent request.

  • The server verifies the JWT and, if it is valid, grants the user access to the requested resources.

  • Once the specified JWT expiry time (1h) elapses, the JWT is no longer considered valid and cannot be used to authenticate the user. The client will need to obtain a new JWT by logging in.

const userModel = require("./models/userModel.js")

exports.signup = async(req, res, next) => {
try{
  const newUser = await userModel.create({
    firstName: req.body.firstName,
    lastName: req.body.lastName,
    email: req.body.email,
    password: req.body.password,
  });

  const token = signToken(newUser._id);
  res.status(201).json({
    status: "success",
    token,
    data: {
      user: newUser,
    },
  });
} catch(error) {
    return next(error);
}
};

Login functionality

  • The user provides their email and password.

  • The server checks if there is any user with the provided credentials in the database.

  • If the user is found, the server compares the provided password and the password linked to the found user in the database.

  • if the passwords are the same, the server creates a JWT that contains the user information and the JWT signing process starts just like in the signup functionality above.

const userModel = require("./models/userModel.js")

exports.login = async (req, res, next) => {
try {
    const { email, password } = req.body;
  if (!email || !password) {
    return next(new AppError("Please provide email and password", 400));
  }
  const user = await userModel.findOne({ email }).select("+password");
  if (!user) {
    return next(new AppError("User not found", 401));
  }
  const validatePass = bcrypt.compare(req.body.password, user.password);
  if (!validatePass) {
    return next(new AppError("Incorrect password or email", 401));
  }
  const token = signToken(user._id);
  res.status(200).json({
    status: "success",
    token,
  });
} catch(error) {
    return next(error);
}
};

Blog management.

Blog management involves implementing various routes and controller functions that allow users to perform various actions related to managing a blog, such as creating new blog articles, editing existing articles, and deleting articles.

We are going to create a file blogController.js where we are going to define all the blog controller functions.

Controller functions for the blog public routes

Get all Published articles

This route is a public route, accessible to all users whether logged in or not. According to our requirement, the list of published articles should be filterable and paginated.

To retrieve a list of published articles from the database, we are going to define a middleware function called getAllArticles .

exports.getAllArticles = blogRouter.GET("/article",async (req, res, next) => {
  const queryObj = { ...req.query };

  // Filtering
  const excludedFields = ["page", "sort", "limit", "fields"];
  excludedFields.forEach((el) => delete queryObj[el]);
  let query = blogModel.find(queryObj);

  // Sorting
  if (req.query.sort) {
    const sortBy = req.query.sort.split(",").join(" ");
    query = query.sort(sortBy);
  } else {
    query = query.sort("-createdAt"); // default sorting : starting from the most recent
  }
  // Pagination
  const page = req.query.page * 1 || 1; // convert to number and set default value to 1
  const limit = req.query.limit * 1 || 20;
  const skip = (page - 1) * limit;

  if (req.query.page) {
    const numArticles = await blogModel
      .countDocuments()
      .where({ state: "published" });
    if (skip >= numArticles)
      throw new Error("This page does not exist", 404);
  }
  query = query.skip(skip).limit(limit);

  // Displaying only published articles
  const publishedArticles = await blogModel
    .find(query)
    .where({ state: "published" })
    .populate("user", { firstName: 1, lastName: 1, _id: 1 });

  res.status(200).json({
    status: "success",
    result: publishedArticles.length,
    current_page: page,
    limit: limit,
    total_pages: Math.ceil(publishedArticles.length / limit),
    data: {
      publishedArticles,
    },
  });
});

The getAllArticles function performs the following tasks:

  • It creates a copy of the query object in the request and stores it in the queryObj variable.

  • It removes certain fields (such as "page", "sort", "limit", and "fields") from the queryObj object.

  • It uses the blogModel.find() function to retrieve all documents in the blog collection that match the criteria specified in the queryObj object.

  • It checks the sort query parameter in the request to see if the user has specified a sort order. If a sort order is specified, the function uses the sort() function to sort the articles accordingly. If no sort order is specified, the function defaults to sorting the articles by the createdAt field in descending order (most recent first).

  • It calculates the current page number and the number of articles to be displayed per page based on the page and limit query parameters in the request. If no page or limit parameters are specified, the function sets the default values to 1 and 20, respectively.

  • It uses the skip() and limit() functions to paginate the articles.

  • It retrieves only the published articles from the database using the find() and where() functions, and populates the user field with the first name, last name, and ID of the user who created the article.

  • It sends a response to the client with a status code of 200, a success message, and a list of articles.

Get a specific published article.

To retrieve a single published article from the database and returns it to the client in the response, we are going to define a middleware function called getArticle .

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

  const article = await blogModel
    .findById(id)
    .where({ state: "published" })
    .populate("user", { firstName: 1, lastName: 1, _id: 1 });

  if (!article) {
    return next(new AppError("Article not found", 404));
  }
  // Updating the read_count
  article.read_count += 1;
  // save to the database
  article.save();
  res.status(200).json({
    status: "success",
    article,
  });
} catch(error){ 
    return next(error)
}  
};

The getArticle function performs the following tasks:

  • Use the findById method on the blogModel object to search the database for a blog post with the specified ID.

  • It then uses the where method to filter the search results to only include published articles, and the populate method to include data from the associated user document in the search results.

  • If the specified article is not found, the function sends a 404 "not found" error to the client using the next function.

  • If the article is found, it increments the read_count field by 1, saves the updated article to the database, and sends the article data to the client in the response.

Controller functions for the Blog Protected routes.

Protecting the routes.

This ensures that only authorized users can access certain resources or perform certain actions. This can help to prevent unauthorized users from tampering with or accessing sensitive data, and can also help to enforce permissions and roles within the application.

To protect some routes in our blog API, we are going to create a authenticate middleware function on the authController.js .

exports.authenticate = async (req, res, next) => {
try {
  let token;
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith("Bearer")
  ) {
    token = req.headers.authorization.split(" ")[1];
  }
  if (!token) {
    return next(new Error("Unauthorized!!!. Please login to continue", 401));
  }
  // Verify the token
  const user = await promisify(jwt.verify)(token, JWT_SECRET);

  // check if the user still exists
  const CurrentUser = await userModel.findById(user.id);
  if (!CurrentUser) {
    return next(new Error("User no longer exists", 401));
  }

  req.user = CurrentUser;
  next();
});  
} catch(error) {
    return next(error);
}

The authenticate function performs the following tasks:

  • Checks the Authorization header in the request for a valid JSON Web Token (JWT).

  • If the Authorization header is present and starts with "Bearer", the function extracts the token and verifies it using the jwt.verify function.

  • If the token is valid, the function checks if the user associated with the token still exists in the database.

  • If the user exists, the user's information is attached to the req object and the request is allowed to continue to the next middleware or route handler.

  • If the token is invalid or the user does not exist, an error is thrown and the request is halted.

req.user is an object that stores the authenticated user's information. It is typically set by the authentication middleware, which verifies the user's credentials and attaches the user's information to the req object.

This req.user object can then be used by the route handler or other middleware functions to perform actions on behalf of the authenticated user, such as creating a new article.

Create Article

To create an article, we are going to define a controller function called createArticle.

exports.createArticle = async (req, res, next) => {
try{
    const { title, description, state, tags, body } = req.body;

  if (!title || !description || !state || !tags || !body) {
    return next(new AppError("Please provide all the required fields", 400));
  }

  // find the user who is creating the blog
  const user = await userModel.findById(req.user._id);
  const time = readingTime(body);
  const reading_time = `${time} min read`;

  // create the blog
  const newArticle = new blogModel({
    title: title,
    description: description,
    author: `${user.firstName} ${user.lastName}`,
    reading_time: reading_time,
    state: state,
    tags: tags,
    body: body,
    user: user._id,
  });

  // save the blog
  const savedArticle = await newArticle.save();
  // add the blog to the user's blogs array
  user.articles = user.articles.concat(savedArticle._id);
 // save the user
  await user.save();
  res.status(201).json({
    status: "success",
    message: "Article created successfully",
    data: {
      blog: savedArticle,
    },
  });
} catch(error){
    return next(error);
}
};

The createArticle function performs the following tasks:

  • It extracts the required fields (title, description, state, tags, body) from the request body.

  • It checks if all the required fields are present. If any of the required fields is missing, it returns an error message to the client.

  • It finds the user who is creating the article by searching for the user with the _id that is stored in the req.user object.

  • It calculates the reading time of the article by using the readingTime function and storing the result in a variable called reading_time.

  • It creates a new article using the blogModel constructor, passing in the required fields as arguments.

  • It saves the new article to the database using the save method.

  • It adds the new article to the user's articles array.

  • It saves the updated user to the database.

  • It sends a success message and the saved article to the client as a response.

Get user articles

To retrieve a list of articles from the database that belong to the user specified in the req.user field, we are going to define a controller function called getUserArticles.

exports.getUserArticles = async (req, res, next) => {
try{
   const user = req.user;
  const articles = await blogModel
    .find({ user: user })
    .populate("user", { firstName: 1, lastName: 1, _id: 1 });

  return res.status(200).json({
    status: "success",
    result: articles.length,
    data: {
      articles: articles,
    },
  });  
} catch(error){
    return next(error)
}
};

The getUserArticles function performs the following tasks:

  • The blogModel.find() function filters the articles by the user field, which is set to the req.user object.

  • The populate() function is used to include the details of the user who created the articles in the response, by specifying the fields to include (firstName, lastName, and _id) in the second argument.

  • Finally, the retrieved articles are sent as a response to the client with a 200 status code.

Update user article.

For users to update their articles, we are going to create a middleware function called updateUserArticle.

exports.updateUserArticle = async (req, res, next) => {
try{
    const { title, description, state, tags, body } = req.body;
  // Getting the logged in user
  const user = req.user;
  // Getting the article with the Id
  const article = await blogModel.findById(req.params.id);
  //Checking if the logged in user is the owner of the blog in order to update it
  if (user.id !== article.user._id.toString())
    return next(
      new Error("You are not authorized to update this article", 401)
    );
  // Updating the blog
  const updatedArticle = await blogModel.findByIdAndUpdate(
    { _id: req.params.id },
    {
      $set: {
        title: title,
        description: description,
        state: state,
        tags: tags,
        body: body,
      },
    },
    {
      new: true,
    }
  );

  res.status(200).json({
    status: "success",
    data: {
      updatedArticle,
    },
  });
} catch(error) {
    return next(error);
}  
};

The function updateUserArticle does the following:

  • Firstly, it destructures the title, description, state, tags, and body properties from the req.body object.

  • It then gets the logged-in user from req.user and the article to be updated from the blogModel based on the id provided in the request parameters.

  • Next, it checks if the logged-in user is the owner of the article by comparing the user's id with the _id of the article's user field.

  • If the user is not the owner, the function sends a 401 error response with a message saying that the user is not authorized to update the article.

  • If the user is the owner, the function updates the article using the findByIdAndUpdate method on the blogModel, setting the new values of the fields provided in the request body, and returning the updated article.

  • Finally, the function sends a 200 status code and the updated article in the response.

Delete user article

To allow a user to delete an article with a specific ID, we are going to define a middleware function called deleteUserArticle.

exports.deleteUserArticle = async (req, res, next) => {
try{
  const user = req.user;
  const article = await blogModel.findById(req.params.id);
  const User = await userModel.findById(user.id);

  if (user.id !== article.user._id.toString())
    return next(
      new Error("You are not authorized to delete this article", 401)
    );
  await blogModel.findByIdAndDelete(req.params.id);

  const index = User.articles.indexOf(req.params.id);
  if (index === -1) return next(new Error("Article not found", 404));
  User.articles.splice(index, 1);
  await User.save();

  res.status(200).json({
    status: "success",
    message: "Article deleted successfully",
  });
} catch(error) {
    return next(error);
}
};

This deleteUserArticle does the following:

  • The function first gets the currently logged-in user and the article with the specified ID.

  • It then checks if the logged-in user is the owner of the article by comparing their ID to the ID of the user who created the article.

  • If the logged-in user is not the owner of the article, the function returns an error message stating that the user is not authorized to delete the article.

  • If the logged-in user is the owner of the article, the function finds the article by its ID and deletes it.

  • It also removes the ID of the deleted article from the user's list of articles and saves the updated list to the database.

  • Finally, the function returns a success message stating that the article was deleted successfully.

We are done with defining the blog controller functions. The next step is to replace the write controller function name here with the appropriate controller function name.

First, import both the authController.js and blogController.js files on the app.js where the routes are defined, and then add the controller functions we defined.

const authController = require("./authController");
const blogController = require("./blogController");

// Defining the routes 
app.post("api/v1/auth/signup", authController.signup); 
app.post("api/v1/auth/login", authController.login); 

// Public routes
app.get("api/v1/blog", blogController.getAllArticles); 
app.get("api/v1/blog/:id", blogController.getArticle); 

// Private routes => Protected routes
app.post("api/v1/blog/articles", blogController.createArticle);  
app.get("api/v1/blog/articles/:id", blogController.getUserArticles);  
app.put("api/v1/blog/articles/:id", blogController.updateUserArticle); 
app.delete("api/v1/blog/articles/:id", blogController.deleteUserArticle);

Next, Let's protect some routes by adding the authenticate function to the routes after the route path.

// Private routes => Protected routes
app.post("api/v1/blog/articles", authController.authenticate, blogController.createArticle);  
app.get("api/v1/blog/articles/:id",  authController.authenticate, blogController.getUserArticles);  
app.put("api/v1/blog/articles/:id",  authController.authenticate, blogController.updateUserArticle); 
app.delete("api/v1/blog/articles/:id",  authController.authenticate,  blogController.deleteUserArticle);

Each of these routes will call the corresponding function in the blogController after first verifying the user's authentication with the authController.authenticate function.

Now, we have successfully implemented the CRUD functionalities.

Adding Validators and Rate-limiting.

Validators are functions that check the input data for an application. They can be used to ensure that the data being inputted into a database is in the correct format, has the correct data types, and meets certain criteria. This helps to prevent errors and ensure that the data being stored is accurate and consistent.

Rate limiting is a technique used to control the amount of incoming and outgoing traffic to or from a network. In a web application, rate limiting can be used to limit the number of requests that a user or client can make to an API or other service within a specified time period. This can help to prevent abuse or overuse of the service, and ensure that it remains available and responsive for all users.

The use of validators and express rate-limiting can greatly improve the security and reliability of your API and is an important consideration when building any API.

You can use either express validators or Joi for your validators and express-rate-limit for rate-limiting setup.

Documentation.

Good documentation is important for any API, as it helps developers understand how to use the API and its various endpoints. You can use tools like Postman to generate API documentation automatically.

Conclusion.

Building a blog API with Express is a straightforward process that can be achieved by following a few simple steps. First, you will need to set up your Express server and define your routes and controllers. You can then implement user management, authentication, and authorization with JSON Web Tokens to protect your routes. You can also include validators to ensure that the data being sent to your API is in the correct format, and you can use express rate-limiting to prevent excessive requests to your API.

It is also important to document these features in the API documentation, along with any other relevant information, to help developers understand how to use the API effectively.

By following these steps, you will have a fully functional and secure blog API built with Express.

You can check out my postman documentation here.

Happy coding!

Credits

Technical writing template by Bolaji Ayodeji

AltschoolAfrica school of Engineering

Jonas Schmedtmann