DEV Community

Precious Abubakar
Precious Abubakar

Posted on

Building a Blog API with Node.js: Designing the API (Part 2)

In part one of this series, we set up our development environment and a basic file structure. Now, let's design the API itself. In this article, we'll cover the following topics:

  • Deciding on the routes and functionality of our API

  • Defining the data models for our database

  • Implementing the data models

  • Setting up the database connection

Let's get started!

Deciding on the Routes and Functionality

The first step in designing our API is to decide on the routes and functionality we want to include.

Let's outline the requirements for our blog:

  • A user should be able to sign up and sign in to the blog app

  • A blog can be in two states: draft and published

  • A user should be able to get a list of published articles, whether logged in or not

  • A user should be able to get a published article, whether logged in or not

  • A logged in user should be able to create an article.

  • When an article is created, it should be in draft state.

  • The author of the article should be able to update the state of the article to published

  • The author of the article should be able to edit the article in draft or published state

  • The author of the article should be able to delete the article in draft or published state

  • The author of the article should be able to get a list of their articles. The endpoint should be paginated and filterable by state

  • Articles created should have title, cover image, description, tags, author, timestamp, state, read count, reading time and body.

  • The list of articles endpoint that can be accessed by both logged in and not logged in users should be paginated.

    • It should be searchable by author, title and tags.
    • It should also be orderable by read count, reading time and timestamp
  • When a single article is requested, the API should return the author's information with the blog, and the read count of the blog should increase by 1.

Considering the outlined requirements, we'll define our routes as follows:

  • Blog routes:

    • GET /blog: Retrieve a list of all published articles
    • GET /blog/:article_id: Retrieve a single article by its ID
  • Author routes: We want only authenticated users to have access to these routes and all of the CRUD operations.

    • GET /author/blog: Retrieve a list of all published articles created by the user.
    • POST /author/blog: Create a new article
    • PATCH /author/blog/edit/:article_id: Update an article by its ID
    • PATCH /author/blog/edit/state/:article_id: Update an article's state
    • DELETE /author/blog/:article_id: Delete an article by its ID
  • Auth routes: for managing user authentication.

    • POST /auth/signup: Register a new user
    • POST /auth/login: Log in an existing user

Defining the Data Models

With the routes defined, we can start thinking about the data models for our database. A data model is a representation of the data that will be stored in the database and the relationships between that data. We'll be using Mongoose to define our schema.

We will have two data models: Blog and User.

User

field data_type constraints
firstname String required
lastname String required
email String required, unique, index
password String required
articles Array, [ObjectId] ref - Blog

Blog

field data_type constraints
title String required, unique, index
description String
tags Array, [String]
imageUrl String
author ObjectId ref - Users
timestamp Date
state String required, enum: ['draft', 'published'], default:'draft'
readCount Number default:0
readingTime String
body String required

Mongoose has a method called populate(), which lets you reference documents in other collections. populate() will automatically replace the specified paths in the document with document(s) from other collection(s). The User model has its articles field set to an array of ObjectId's. The ref option is what tells Mongoose which model to use during population, in this case the Blog model. All _id's we store here must be article _id's from the Blog model. Similarly, the Blog model references the User model in its author field.

Implementing the data models

  • In /src/models, create a file called blog.model.js and set up the Blog model:
const mongoose = require("mongoose");
const uniqueValidator = require('mongoose-unique-validator');

const { Schema } = mongoose;

const BlogSchema = new Schema({
    title: { type: String, required: true, unique: true, index: true },
    description: String,
    tags: [String],
    author: { type: Schema.Types.ObjectId, ref: "Users" },
    timestamp: Date,
    imageUrl: String,
    state: { type: String, enum: ["draft", "published"], default: "draft" },
    readCount: { type: Number, default: 0 },
    readingTime: String,
    body: { type: String, required: true },
});

// Apply the uniqueValidator plugin to the blog model
BlogSchema.plugin(uniqueValidator);

const Blog = mongoose.model("Blog", BlogSchema);

module.exports = Blog;
Enter fullscreen mode Exit fullscreen mode

The title field is defined as a required string and must be unique across all documents in the collection. The description field is defined as a string, and the tags field is defined as an array of strings. The author field is defined as a reference to a document in the users collection, and the timestamp field is defined as a date. The imageUrl field is defined as a string, the state field is defined as a string with a set of allowed values (either "draft" or "published"), and the readCount field is defined as a number with a default value of 0. The readingTime field is defined as a string, and the body field is defined as a required string.

mongoose-unique-validator is a plugin that adds pre-save validation for unique fields within a Mongoose schema. It will validate the unique option in the schema and prevent the insertion of a document if the value of a unique field already exists in the collection.

  • In /src/models, create a file called user.model.js and set up the User model:
const mongoose = require("mongoose");
const uniqueValidator = require("mongoose-unique-validator");
const bcrypt = require("bcrypt");

const { Schema } = mongoose;

const UserModel = new Schema({
    firstname: { type: String, required: true },
    lastname: { type: String, required: true },
    email: {
        type: String,
        required: true,
        unique: true,
        index: true,
    },
    password: { type: String, required: true },
    articles: [{ type: Schema.Types.ObjectId, ref: "Blog" }],
});

// Apply the uniqueValidator plugin to the user model
UserModel.plugin(uniqueValidator);

UserModel.pre("save", async function (next) {
    const user = this;

    if (user.isModified("password") || user.isNew) {
        const hash = await bcrypt.hash(this.password, 10);

        this.password = hash;
    } else {
        return next();
    }
});

const User = mongoose.model("Users", UserModel);

module.exports = User;
Enter fullscreen mode Exit fullscreen mode

The firstname and lastname fields are defined as required strings, and the email field is defined as a required string and must be unique across all documents in the collection. The password field is defined as a required string, and the articles field is defined as an array of references to documents in the Blog collection.

The pre hook is used to add a function that will be executed before a specific Mongoose method is run. The pre-save hook here hashes the user's password with the npm module bcrypt, before the user document is saved to the database.

Setting Up the Database Connection

Now that we have our routes and data models defined, it's time to set up the database connection.

  • Set up your MongoDB database and save the connection url in your .env file.

  • Run the following command to install the npm package mongoose:

npm install --save mongoose
Enter fullscreen mode Exit fullscreen mode
  • Create a file called db.js in the /database directory. In /database/db.js, set up the database connection using Mongoose:
const mongoose = require('mongoose');

const connect = (url) => { 
mongoose.connect(url || 'mongodb://[localhost:27017](http://localhost:27017)')

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

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

module.exports = { connect };
Enter fullscreen mode Exit fullscreen mode

The connect function takes an optional url argument, which specifies the URL of the database to connect to. If no URL is provided, it defaults to 'mongodb://localhost:27017', which connects to the MongoDB instance running on the local machine at the default port (27017).

  • Create an index.js file in the /database directory:
const database = require("./db");

module.exports = {
  database,
};
Enter fullscreen mode Exit fullscreen mode

Now that we have the database connection set up, in the next article, we'll dive into two important concepts- authentication and data validation. Stay tuned!

Top comments (0)