An introduction to CQRS
So far, in our application, we’ve been following a pattern of controllers using services to access and modify the data. While it is a very valid approach,…
So far, in our application, we’ve been following a pattern of controllers using services to access and modify the data. While it is a very valid approach, there are other possibilities to look into. NestJS suggests command-query responsibility segregation (CQRS). In this article, we look into this concept and implement it into our application. Instead of keeping our logic in services, with CQRS, we use commands to update data and queries to read it. Therefore, we have a separation between performing actions and extracting data. While this might not be beneficial for simple CRUD applications, CQRS might make it easier to incorporate a complex business logic. Doing the above forces us to avoid mixing domain logic and infrastructural operations. Therefore, it works well with Domain-Driven Design. Domain-Driven Design is a very broad topic and it will be covered separately Implementing CQRS with NestJS The very first thing to do is to install a new package. It includes all of the utilities we need in this article.npm install --save @nestjs/cqrsLet’s explore CQRS by creating a new module in our application that we’ve been working on in this series. This time, we add a comments module. comment.entity.ts import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import User from '../users/user.entity'; import Post from '../posts/post.entity'; @Entity() class Comment { @PrimaryGeneratedColumn() public id: number; @Column() public content: string; @ManyToOne(() => Post, (post: Post) => post.comments) public post: Post; @ManyToOne(() => User, (author: User) => author.posts) public author: User; } If you want to know more on creating entities with relationships, check out API with NestJS #7. Creating relationships with Postgres and TypeORM createComment.dto.ts import { IsString, IsNotEmpty, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; import ObjectWithIdDTO from 'src/utils/types/objectWithId.dto'; export class CreateCommentDto { @IsString() @IsNotEmpty() content: string; @ValidateNested() @Type(() => ObjectWithIdDTO) post: ObjectWithIdDTO; } export default CreateCommentDto; We tackle the topic of validating DTO classes in API with NestJS #4. Error handling and data validation Executing commands With CQRS, we perform actions by executing commands. We first need to define them. createComment.command.ts import CreateCommentDto from '../../dto/createComment.dto'; import User from '../../../users/user.entity'; export class CreateCommentCommand { constructor( public readonly comment: CreateCommentDto, public readonly author: User, ) {} }To execute the above command, we need to use a command bus. Although the official documentation suggests that we can create services, we can execute commands straight in our controllers. In fact, this is what the creator of NestJS does during his talk at JS Kongress. comments.controller.ts import { Body, ClassSerializerInterceptor, Controller, Post, Req, UseGuards, UseInterceptors, } from '@nestjs/common'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; import CreateCommentDto from './dto/createComment.dto'; import { CommandBus } from '@nestjs/cqrs'; import { CreateCommentCommand } from './commands/implementations/createComment.command'; @Controller('comments') @UseInterceptors(ClassSerializerInterceptor) export default class CommentsController { constructor(private commandBus: CommandBus) {} @Post() @UseGuards(JwtAuthenticationGuard) async createComment(@Body() comment: CreateCommentDto, @Req() req: RequestWithUser) { const user = req.user; return this.commandBus.execute( new CreateCommentCommand(comment, user) ) } } Above, we use the fact that the user that creates the comment is authenticated. We tackle this issue in API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies Once we execute a certain command, it gets picked up by a matching command handler. createComment.handler.ts import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CreateCommentCommand } from '../implementations/createComment.command'; import { InjectRepository } from '@nestjs/typeorm'; import Comment from '../../comment.entity'; import { Repository } from 'typeorm'; @CommandHandler(CreateCommentCommand) export class CreateCommentHandler implements ICommandHandler