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_migrationstable. 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-commentsThis 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:

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:

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 modulemodel.ts: This file will contain the model definition for the modulerepository.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.
- The model needs to be added inside the sequelize instance. To do this, we need to import the model in the
src/database/index.tsfile and add it to themodelsobject.

- We need to create a new route and expose the controller methods. Create a new file called
comments.tsinside thesrc/api/routesdirectory. 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 }- We need to import this new route inside the
src/indexfile 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.