Build a RESTful API for Managing Books with Node.js and Sequelize(MYSQL)

In this blog, we will build a RESTful Book Directory API using Node.js, Express, and Sequelize with Swagger API documentation. I am also sharing the project's Source code for your reference: https://github.com/DhananjayThomble/Book-Directory-API

Sequelize is an ORM(Object Relational Mapper) for Node.js, and it provides many features such as synchronization of models with database, defining database schema, mapping HTTP requests to database operations, and more.

  • Building a RESTful API that includes Swagger documentation is an essential component of many modern web applications. Swagger documentation provides a user-friendly way to explore and interact with your API, enabling developers to better understand your API’s capabilities and how to use it.

  • We will create a Book model, router, and index file. The Book model will be used to define the structure of the table. The router will be used to define the RESTful endpoints, and the index file will be used to define the server.

Prerequisites:

  • Basic knowledge of Node.js, Express, and Sequelize.

  • A text editor or IDE installed on your computer.

  • A MySQL database installed on your computer(you can also use XAMPP on Windows).

Steps:

1: Setup the project:

  1. First, let’s create a new directory for our project and navigate into it using the command line.

     mkdir book-directory-api
     cd book-directory-api
    

    Next, we will create a new package.json file using the following command:

     npm init -y
    

    This will create a new package.json file in your project directory with default values. The -y flag is optional, basically it skips the prompt for asking about package details.

2: Install the required dependencies:

We will now install the required dependencies for our project. We will be using express, body-parser, sequelize, mysql2, dotenv, swagger-ui-express, and yamljs.

npm install express body-parser sequelize mysql2 dotenv swagger-ui-express yamljs
  • express: A Node.js web application framework for building web applications.

  • body-parser: A middleware that parses incoming request bodies in a middleware before your handlers, available under the req.body property.

  • sequelize: A promise-based ORM for Node.js, that supports PostgreSQL, MySQL, SQLite and MSSQL.

  • mysql2: A MySQL client for Node.js with focus on performance.

  • dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.

  • swagger-ui-express: A middleware that serves auto-generated Swagger UI documentation from your API endpoints.

  • yamljs: A library for loading YAML files.

3: Create a MySQL database:

Next, we will create a MySQL database named 'bookDB' that we will use for our application. You can use any database name of your choice.

mysql -u root -p

You can also use phpMyAdmin or any other MySQL GUI tool to create a database.

CREATE DATABASE bookDB;

now exit from MySQL terminal by entering the ' exit; ' command.

4. Create the config file and add the database credentials:

touch config/database.js  # create a new file named database.js in config directory

'touch' command will not work in the Windows terminal, but you can manually create the folder 'config' and inside that 'config' folder, create a new file 'database.js'.

Open the database.js file in your text editor/IDE and add the following code:

// sequelize configuration
const {Sequelize} = require("sequelize");

// MYSQL connection configuration
const sequelize = new Sequelize(
    process.env.DATABASE,
    process.env.DATABASE_USER,
    process.env.DATABASE_PASSWORD,
    {
        host: process.env.DATABASE_HOSTNAME,
        dialect: "mysql",
    }
);

module.exports = sequelize;

5: Create the Book model:

We will create a new file named BookModel.js in a new directory named models.

The BookModel.js file will define the structure of the Book table in the database. It will also create the table in the database using the sync() method. We will use Sequelize to define the model's properties such as id, title, author, and release_date.

mkdir models
touch models/BookModel.js

Open the BookModel.js file in your text editor/IDE and add the following code:

const sequelize = require("../config/database");
const { DataTypes } = require("sequelize"); 

// Model: It is used to define the structure of the table.
const BookModel = sequelize.define("Book", {
    id: {
        type: DataTypes.UUID,   // used to generate unique id
        primaryKey: true,
        defaultValue: DataTypes.UUIDV4,
    },
    title: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    author: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    release_date: {
        type: DataTypes.DATE,
        allowNull: false,
    },
});

// Model End

module.exports = BookModel;

6: Create the Book router:

touch routes/BookRouter.js

The bookRouter.js file will handle all the CRUD operations for books. It will use Sequelize to interact with the database. It will also use Express middleware to validate the request body and query parameters.

Open the BookRouter.js and add the following code:

const express = require("express");
const bookRouter = express.Router();
const BookModel = require("../models/BookModel");
const isEmpty = require("../util/isEmpty");

// get all books sorted by title
bookRouter.get("/", async (req, res) => {
  const books = await BookModel.findAll({
    order: [["title", "ASC"]],
  });
  res.status(200).json(books);
});

// Insert a book into database
bookRouter.post("/", isEmpty, async (req, res) => {
  const { title, author, release_date } = req.body;

  // check if release_date is valid or not
  if (isNaN(Date.parse(release_date))) {
    return res.status(400).json({ message: "Invalid release_date" });
  }

  // check if book already exists
  const bookExists = await BookModel.findOne({ where: { title } });
  if (bookExists)
    return res.status(409).json({ message: "Book already available" });
  // create/insert new book
  const book = await BookModel.create({
    title,
    author,
    release_date,
  });
  res.status(201).json(book);
});

// get a book by title
bookRouter.get("/:title", async (req, res) => {
  const { title } = req.params;
  const book = await BookModel.findOne({ where: { title } });
  if (!book) return res.status(404).json({ message: "Book not found" });
  res.status(200).json(book);
});

// update a book by title
bookRouter.put("/:title", isEmpty, async (req, res) => {
  const { title } = req.params;
  const { newTitle, author, release_date } = req.body;

  // check if release_date is valid or not
  if (isNaN(Date.parse(release_date))) {
    return res.status(400).json({ message: "Invalid release_date" });
  }
  /*
    Date.parse() returns the number of milliseconds since 1 January 1970 00:00:00 UTC.
    If the date is invalid, it returns NaN. NaN stands for Not a Number.
  */

  //   console.log(newTitle);
  const book = await BookModel.update(
    { title: newTitle, author, release_date },
    { where: { title } }
  );
  // console.log(book);
  if (book[0] === 0) return res.status(404).json({ message: "Book not found" });
  res.status(200).json(book); // returns 1 if updated // returns 0 if not updated
});

// delete a book by title
bookRouter.delete("/:title", async (req, res) => {
  const { title } = req.params;
  // check if book exists or not
  const bookExists = await BookModel.findOne({ where: { title } });
  if (!bookExists) return res.status(404).json({ message: "Book not found" });
  // delete book
  const book = await BookModel.destroy({ where: { title } });
  if (book === 1)
    res.status(200).json({ message: "Book deleted successfully" });
});

module.exports = bookRouter;

Now Create 'isEmpty' middleware to check whether the request body is empty or not.

mkdir util
touch util/isEmpty.js

Open the isEmpty.js and add the following code:

// middleware to check whether the request body is empty or not

// check if request body params are empty or not
const isEmpty = (req, res, next) => {
    /*
        Object.keys() returns an array of a given object's own enumerable property names.
        Syntax: Object.keys(obj)
        Parameters: obj: The object whose enumerable and non-enumerable properties are to be returned.
        Return value: An array of strings that represent all the enumerable properties of the given object.
        Example:
        const object1 = {
            name: 'john doe',
            age: 42,
            gender: 'male'
        };
        console.log(Object.keys(object1));
        // expected output: Array ["name", "age", "gender"]
    * */
  if (Object.keys(req.body).length === 0) {
    return res.status(400).json({ message: "Request body cannot be empty" });
  }
  // check all the request body params are empty or not
  for (let key in req.body) {
    if (req.body[key] === "") {
      return res.status(400).json({ message: "Request body cannot be empty" });
    }
  }
  next();   // if all the request body params are not empty then call next middleware.
            // here 'next middleware' means the next function in the route
};

module.exports = isEmpty;

7: Create the index.js file:

touch index.js

Open the index.js file and add the following code:

// dotenv
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const swaggerUi = require("swagger-ui-express"); // for documentation
const yamljs = require("yamljs");
const sequelize = require("./config/database");

const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());

// Sequelize connection
const connectToDatabase = async () => {
    try {
        await sequelize.authenticate();
        console.log("Connection has been established successfully.");
    } catch (error) {
        console.error("Unable to connect to the database: ", error);
    }
};
connectToDatabase().then(() => {
    // Sync: It is used to create the table in the database. It will create the table only if it does not exist.
    // if any changes are made to the model, it will update the table.
    sequelize.sync().then(() => {
        console.log("Database & tables created!");
    });
});

// API documentation
const swaggerDocument = yamljs.load("./swagger.yaml");
app.use("/doc", swaggerUi.serve, swaggerUi.setup(swaggerDocument));

app.use("/api", require("./routes/bookRouter"));

app.get("/", (req, res) => {
    // redirect to documentation
    res.redirect("/doc");
});

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

8: Create the swagger.yaml file:

touch swagger.yaml

Open the swagger.yaml file and add the following code:

openapi: 3.0.0
info:
  title: Book Directory API
  description: API for managing books in MYSQL database using Sequelize ORM
  version: 1.0.0

servers:
  - url: http://localhost:3000
    description: Local server

paths:
  /api/:
    get:
      summary: Get all books in alphabetical order by title
      description: Get all books
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    title:
                      type: string
                    author:
                      type: string
                    release_date:
                      type: string
                    createdAt:
                      type: string
                    updatedAt:
                      type: string

    post:
      summary: Create a book
      description: Create a book in the database
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                author:
                  type: string
                release_date:
                  type: string
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  title:
                    type: string
                  author:
                    type: string
                  release_date:
                    type: string
                  createdAt:
                    type: string
                  updatedAt:
                    type: string
        "409":
          description: Book already exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Book already exists

  /api/{title}:
    get:
      summary: Get book by title
      description: Get book by title
      parameters:
        - in: path
          name: title
          schema:
            type: string
          required: true
          description: Title of the book
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  title:
                    type: string
                  author:
                    type: string
                  release_date:
                    type: string
                  createdAt:
                    type: string
                  updatedAt:
                    type: string

        "404":
          description: Book not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Book not found

    put:
      summary: Update book by title
      description: Update book by title
      parameters:
        - in: path
          name: title
          schema:
            type: string
          required: true
          description: Title of the book
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                author:
                  type: string
                release_date:
                  type: string

      responses:
        "200":
          description: Book updated successfully
          content:
            application/json:
              schema:
                type: number
                example: 1
        "404":
          description: Book not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Book not found
    delete:
      summary: Delete book by title
      description: Delete book by title
      parameters:
        - in: path
          name: title
          schema:
            type: string
          required: true
          description: Title of the book
      responses:
        "200":
          description: Book deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Book deleted successfully
        "404":
          description: Book not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Book not found

9: Create the .env file:

touch .env

Open the .env file and add the following code:

PORT=3000
DATABASE=PutYourDatabaseNameHere
DATABASE_USER=PutYourDatabaseUserNameHere
DATABASE_PASSWORD=PutYourDatabasePasswordHere
DATABASE_HOSTNAME=PutYourDatabaseHostNameHere

Change the values of the environment variables according to your database credentials.

10: Run the application:

npm start

Now go to http://localhost:3000/doc to see the documentation and test our APIs.

Did you find this article valuable?

Support Dhananjay Thomble by becoming a sponsor. Any amount is appreciated!