Quick Start
NodeJS Server
Tutorials
Add a new module

Add a new module

We will be adding a comments module to the project. This module will allow users to add comments to an auction.

Add the database table

If you are already running the server, make sure you stop it before proceeding with the next steps, as the server will detect any change inside the code and it will try to apply the changes to the database. A migration is always ran only once.

If you are ever in a situation in which a database migration already ran, but you need it to run it again, you can remove the migration straight from the database. The migrations that ran are stored in the sequelize_migrations table. You can remove the migration from the table and run the server again. The server will detect that the migration is missing and it will run it again.

Before adding the new module, we need to make sure that a database table is created for the module. To create a new table, we need to create a new migration file in the migrations directory. In order to autimatically generate a migration file, we need to open a terminal in the project directory and run the following command:

npm run migration:create add-comments

This command will create a new migration file in the migrations directory. The migration file will contain only the skeleton of the migration. we need to add the actual migration code to the file.

You will notice that the file has two functions: up and down. Each of the functions has a try/catch statement, place in which we need to add the actual migration code. Inside the up function we will add the code that will create the comments table, and inside the down function we will add the code that will drop the comments table.

export async function up({
  context: queryInterface,
}: {
  context: sequelize.QueryInterface
}) {
  const transaction = await queryInterface.sequelize.transaction()
 
  try {
    await queryInterface.createTable(
      DATABASE_MODELS.COMMENTS,
      {
        id: {
          type: DataTypes.UUID,
          defaultValue: sequelize.literal('gen_random_uuid()'),
          primaryKey: true,
        },
        accountId: {
          allowNull: false,
          type: DataTypes.UUID,
          references: {
            model: DATABASE_MODELS.ACCOUNTS,
            key: 'id',
          },
        },
        auctionId: {
          type: DataTypes.UUID,
          references: {
            model: DATABASE_MODELS.AUCTIONS,
            key: 'id',
          },
        },
        comment: {
          type: DataTypes.TEXT,
          allowNull: false,
        },
        createdAt: {
          allowNull: false,
          type: DataTypes.DATE,
        },
        updatedAt: {
          allowNull: false,
          type: DataTypes.DATE,
        },
      },
      { transaction }
    )
    await transaction.commit()
  } catch (error) {
    console.error(error)
    await transaction.rollback()
    throw error
  }
}
export async function down({
  context: queryInterface,
}: {
  context: sequelize.QueryInterface
}) {
  const transaction = await queryInterface.sequelize.transaction()
 
  try {
    await queryInterface.dropTable(DATABASE_MODELS.COMMENTS, { transaction })
    await transaction.commit()
  } catch (error) {
    await transaction.rollback()
    throw error
  }
}

You can see below an image with the migration file opened in VSCode:


Add database table

You will notice the DATABASE_MODELS object in the migration file. This object contains the names of the tables in the database. You can find the definition of this object in the src/constants/model-names.ts file. Make sure to add the name of the new table to the DATABASE_MODELS object, like this:


Add database table name

The new table will be automatically added to the database when you run the server with the docker-compose up command.

Add the module to the project

To add a new module to the project, we need to create a new directory in the modules directory. The directory name should be the name of the module you want to create. For example, if we are creating a comments module, we need to create a comments directory in the modules directory.

Inside the new directory, we will create 3 files:

  • controller.ts: This file will contain the controller logic for the module
  • model.ts: This file will contain the model definition for the module
  • repository.ts: This file will contain the repository logic for the module

Here is the content we need to add in each of the files:

controller.ts

import { Request, Response } from 'express'
import { GENERAL } from '../../constants/errors.js'
import { CommentsRepository } from './repository.js'
 
export class CommentsController {
  public static async getForAuction(req: Request, res: Response) {
    const { auctionId } = req.params
 
    try {
      const comments = await CommentsRepository.getForAuction(auctionId)
      res.status(200).send(comments)
    } catch (error) {
      console.error(error)
      res.status(500).send({ error: GENERAL.SOMETHING_WENT_WRONG })
    }
  }
 
  public static async create(req: Request, res: Response) {
    const { account } = res.locals
    const { auctionId, comment } = req.body
 
    try {
      if (!auctionId || !comment) {
        return res.status(400).send({ error: GENERAL.BAD_REQUEST })
      }
 
      const newComment = await CommentsRepository.create({
        accountId: account.id,
        auctionId,
        comment,
      })
 
      res.status(201).send(newComment)
    } catch (error) {
      console.error(error)
      res.status(500).send({ error: GENERAL.SOMETHING_WENT_WRONG })
    }
  }
 
  public static async update(req: Request, res: Response) {
    const { account } = res.locals
    const { id } = req.params
    const { comment } = req.body
 
    try {
      if (!comment) {
        return res.status(400).send({ error: GENERAL.BAD_REQUEST })
      }
 
      const existingComment = await CommentsRepository.getOneById(id)
      if (!existingComment) {
        return res.status(404).send({ error: GENERAL.NOT_FOUND })
      }
 
      if (existingComment.accountId !== account.id) {
        return res.status(403).send({ error: GENERAL.FORBIDDEN })
      }
 
      const updatedComment = await CommentsRepository.update(
        { where: { id } },
        { comment }
      )
 
      res.status(200).send(updatedComment)
    } catch (error) {
      console.error(error)
      res.status(500).send({
        error: GENERAL.SOMETHING_WENT_WRONG,
      })
    }
  }
 
  public static async delete(req: Request, res: Response) {
    const { account } = res.locals
    const { id } = req.params
 
    try {
      const existingComment = await CommentsRepository.getOneById(id)
      if (!existingComment) {
        return res.status(404).send({ error: GENERAL.NOT_FOUND })
      }
 
      if (existingComment.accountId !== account.id) {
        return res.status(403).send({ error: GENERAL.FORBIDDEN })
      }
 
      await CommentsRepository.deleteById(id)
 
      res.status(204).send()
    } catch (error) {
      console.error(error)
      res.status(500).send({
        error: GENERAL.SOMETHING_WENT_WRONG,
      })
    }
  }
}
 

model.ts

import { DataTypes, Model } from 'sequelize'
import { getModelConfig } from '../../utils/db.js'
import { DATABASE_MODELS } from '../../constants/model-names.js'
import sequelize from 'sequelize'
import { Auction } from '../auctions/model.js'
import { Account } from '../accounts/model.js'
 
export class Comment extends Model {
  declare id: string
  declare auctionId: string
  declare accountId: string
  declare comment: string
 
  declare readonly createdAt: Date
  declare readonly updatedAt: Date
 
  static initModel = initModel
  static initAssociations = initAssociations
}
 
function initModel(): void {
  const modelConfig = getModelConfig(DATABASE_MODELS.COMMENTS)
 
  Comment.init(
    {
      id: {
        type: DataTypes.UUID,
        defaultValue: sequelize.literal('gen_random_uuid()'),
        primaryKey: true,
      },
      accountId: {
        allowNull: false,
        type: DataTypes.UUID,
        references: {
          model: DATABASE_MODELS.ACCOUNTS,
          key: 'id',
        },
      },
      auctionId: {
        type: DataTypes.UUID,
        references: {
          model: DATABASE_MODELS.AUCTIONS,
          key: 'id',
        },
      },
      comment: {
        type: DataTypes.TEXT,
        allowNull: false,
      },
      createdAt: {
        allowNull: false,
        type: DataTypes.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: DataTypes.DATE,
      },
    },
    modelConfig
  )
}
 
function initAssociations() {
  Comment.belongsTo(Auction, {
    foreignKey: 'auctionId',
    onDelete: 'cascade',
  })
 
  Comment.belongsTo(Account, {
    foreignKey: 'accountId',
    onDelete: 'cascade',
  })
}
 

repository.ts

import { GenericRepository } from '../../lib/base-repository'
import { Account } from '../accounts/model'
import { Comment } from './model'
 
class CommentsRepository extends GenericRepository<Comment> {
  constructor() {
    super(Comment)
  }
 
  public async getForAuction(auctionId: string) {
    return Comment.findAll({
      where: { auctionId },
      include: [
        {
          model: Account,
          attributes: ['id', 'name', 'email', 'picture'],
        },
      ],
    })
  }
}
 
const commentsRepositoryInstance = new CommentsRepository()
Object.freeze(commentsRepositoryInstance)
 
export { commentsRepositoryInstance as CommentsRepository }

Notice that the CommentsRepository extends the GenericRepository, which already comes with the basic CRUD operations. The GenericRepository is a class that contains the basic CRUD operations for a model. You can find the definition of the GenericRepository in the src/lib/base-repository.ts file.


After we have created the files, there are a few more this we need to do before the new module is exposed from the server.

  1. The model needs to be added inside the sequelize instance. To do this, we need to import the model in the src/database/index.ts file and add it to the models object.

Sequelize
  1. We need to create a new route and expose the controller methods. Create a new file called comments.ts inside the src/api/routes directory. The file should contain the following code:
import { Router } from 'express'
import { Authenticator } from '../middlewares/auth.js'
import { HttpRateLimiter } from '../middlewares/rate-limiter.js'
import { CommentsController } from '../../modules/comments/controller.js'
 
const commentsRouter = Router()
commentsRouter.use(await Authenticator.authenticateHttp())
commentsRouter.use(HttpRateLimiter.limitRequestsForUser)
 
commentsRouter.get('/:auctionId', CommentsController.getForAuction)
commentsRouter.post('/', CommentsController.create)
commentsRouter.put('/:id', CommentsController.update)
commentsRouter.delete('/:id', CommentsController.delete)
 
export { commentsRouter }
  1. We need to import this new route inside the src/index file and add the following line of code, near the other route imports:
app.use('/comments', commentsRouter)

This is it. The new module is now exposed from the server. You can test the new module by using Postman or any other API testing tool, after you start the server with the docker-compose up command.