TODO application with CQRS Design Pattern within Nest JS

Introduction

CQRS (Command Query Responsibility Segregation) is a software design pattern which separates the responsibility of reading and writing data in system. In traditional design pattern often, same set of components are used for reading and writing of data. CQRS proposes the segregation of these responsibilities by introducing two separate models

  1. Command Model: The command model is responsible for processing and validating commands, updating the data store, and triggering events.
  2. Query Model: The Query Model handles operations to retrieve data from the system.

CQRS Have mainly four key concepts as below

  1. Command: Event or request to change the state of system
  2. Query: Event or request to retrieve data from system
  3. Command Handler: A component responsible for handling commands and updating the state of the system.
  4. Query Handler: A component responsible for handling queries and retrieving data from the system.

CQRS Provides Benefits such as

  1. Scalability: Application can be scalable as it supports different model for read and write data.
  2. Flexibility: It allows utilization of separate data stores optimized for reading and writing operations.
  3. Performance: Application performance can be improved.

Nest JS allows to use this CQRS Model, we will learn how to implement that in Nest JS in this article.

We will take example of creating and fetching TODO application with the help of CQRS pattern.

TODO application with CQRS pattern

If nest cli is not installed globally in your system you need to install it using command

npm i -g @nestjs/cli

Create new Nest Js project if not created already using below command

nest new todo-application

Dependency Installation

Nest JS provides a package to implement CQRS, we need to install the package first using below command

npm install --save @nestjs/cqrs

Module Creation

Create new Module for our TODO application using command

nest generate module todo

Commands

Commands are used to change the application state, when a command is triggered, it is handled by corresponding command handler. Then handler will be responsible to process the operation. Every command will have command handler in order to process the command

Create new file inside todo module with name create-todo-command.ts and implement the below code

import { ICommand } from '@nestjs/cqrs';

export class CreateToDoCommand implements ICommand {
    constructor(
      public readonly title: string,
      public readonly description: string,
    ) {}
}

Command Handler

Create new file inside todo module with name create-todo-command-handler.ts  and implement the below code

import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateToDoCommand } from './create-todo-command';

@CommandHandler(CreateToDoCommand)
export class CreateToDoHandler implements ICommandHandler<CreateToDoCommand> {
  async execute(command: CreateToDoCommand): Promise<void> {
    // Add Logic to do validation and business rule
    const { title, description } = command;

    // Use Repository to save directly or Create Factory to add business logic and save
  }
}

Query

Queries are used to retrieve the data from the application, when a query is requested, Query handler handles the requests and retrieves the data. Every query will have query handler

Create new file inside todo module with name get-todo-query.ts  and implement the below code

import {  IQuery } from '@nestjs/cqrs';

export class GetToDoQuery implements IQuery {
    constructor() {}
}

Query Handler

Create new file inside todo module with name get-todo-query-handler.ts  and implement the below code

import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetToDoQuery } from './get-todo-query';

@QueryHandler(GetToDoQuery)
export class GetToDoQueryHandler implements IQueryHandler<GetToDoQuery> {
  async execute(query: GetToDoQuery): Promise<any> {
    // Fetch data using repository or factory and return it
    // Sample Response
    return [
      { id: 1, title: 'Test', description: 'Reminder to complete daily activity' },
      { id: 2, title: 'Test 2', description: 'Reminder to complete daily activity2' },
    ];
  }
}

Module

In the todo.module.ts  file, import the CQRS module to use the command handlers and query handlers

import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { ToDoController } from './todo.controller';
import { CreateToDoHandler } from './create-todo-command-handler';
import { GetToDoQueryHandler } from './get-todo-query-handler';

@Module({
  imports: [CqrsModule],
  controllers: [ToDoController],
  providers: [CreateToDoHandler, GetToDoQueryHandler],
})
export class ToDoModule {}

Controller

Create todo.controller.ts  file to handle the API request and use command and query bus

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CreateToDoCommand } from './create-todo-command';
import { GetToDoQuery } from './get-todo-query';

@Controller('ToDo')
export class ToDoController {
  constructor(
    private readonly commandBus: CommandBus,
    private readonly queryBus: QueryBus,
  ) {}

  @Post()
  async createToDo(@Body() body: { title: string, description: string }): Promise<void> {
    const { title, description } = body;
    await this.commandBus.execute(new CreateToDoCommand(title, description));
  }

  @Get()
  async getToDo(): Promise<any[]> {
    return this.queryBus.execute(new GetToDoQuery());
  }
}

Pro Tips

You can create a separate folder for command and query to segregate the code and make it more readable. Also, here I just gave the basic high level demo, but in your application, you can create dto for command and query, and you can directly use that dto

Conclusion

In conclusion, while CQRS can provide advantages in terms of scalability and flexibility, it comes with increased complexity and potential challenges in terms of consistency and development overhead. It is important to carefully consider whether the benefits align with the specific requirements and goals of the application being developed.


Similar Articles