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,…
December 7, 2020
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
}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 CreateCommentDtoExecuting commands#
With CQRS, we perform actions by executing commands. We first need to define them.
createComment.command.ts#
import User from "../../../users/user.entity"
import CreateCommentDto from "../../dto/createComment.dto"
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))
}
}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<CreateCommentCommand> {
constructor(
@InjectRepository(Comment)
private commentsRepository: Repository<Comment>,
) {}
async execute(command: CreateCommentCommand) {
const newPost = await this.commentsRepository.create({
...command.comment,
author: command.author,
})
await this.commentsRepository.save(newPost)
return newPost
}
}The CreateCommentHandler invokes the execute method as soon as the CreateCommentCommand is executed. It does so, thanks to the fact that we’ve used the @CommandHandler(CreateCommentCommand) decorator.
We need to put all of the above in a module. Please notice that we also import the CqrsModule here.
comments.module.ts#
import { Module } from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm"
import Comment from "./comment.entity"
import CommentsController from "./comments.controller"
import { CqrsModule } from "@nestjs/cqrs"
import { CreateCommentHandler } from "./commands/handlers/create-comment.handler"
@Module({
imports: [TypeOrmModule.forFeature([Comment]), CqrsModule],
controllers: [CommentsController],
providers: [CreateCommentHandler],
})
export class CommentsModule {}Doing all of that gives us a fully functional controller that can add comments through executing commands. Once we execute the commands, the command handler reacts to it and performs the logic that creates a comment.
Querying data#
Another important aspect of CQRS is querying data. The official documentation does not provide an example, but a Github repository can be used as such.
Let’s start by defining our query. Just as with commands, queries can also carry some additional data.
getComments.query.ts#
export class GetCommentsQuery {
constructor(public readonly postId?: number) {}
}To execute a query, we need an instance of the QueryBus. It acts in a very similar way to the CommandBus.
comments.controller.ts#
import {
Body,
ClassSerializerInterceptor,
Controller,
Get,
Post,
Query,
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, QueryBus } from "@nestjs/cqrs"
import { CreateCommentCommand } from "./commands/implementations/createComment.command"
import { GetCommentsQuery } from "./queries/implementations/getComments.query"
import GetCommentsDto from "./dto/getComments.dto"
@Controller("comments")
@UseInterceptors(ClassSerializerInterceptor)
export default class CommentsController {
constructor(
private commandBus: CommandBus,
private queryBus: QueryBus,
) {}
@Post()
@UseGuards(JwtAuthenticationGuard)
async createComment(@Body() comment: CreateCommentDto, @Req() req: RequestWithUser) {
const user = req.user
return this.commandBus.execute(new CreateCommentCommand(comment, user))
}
@Get()
async getComments(@Query() { postId }: GetCommentsDto) {
return this.queryBus.execute(new GetCommentsQuery(postId))
}
}When we execute the query, the query handler picks it up.
getComments.handler.ts#
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetCommentsQuery } from '../implementations/getComments.query';
import { InjectRepository } from '@nestjs/typeorm';
import Comment from '../../comment.entity';
import { Repository } from 'typeorm';
@QueryHandler(GetCommentsQuery)
export class GetCommentsHandler implements IQueryHandler<GetCommentsQuery> {
constructor(
@InjectRepository(Comment)
private commentsRepository: Repository<Comment>,
) {}
async execute(query: GetCommentsQuery) {
if (query.postId) {
return this.commentsRepository.find({
post: {
id: query.postId
}
});
}
return this.commentsRepository.find();
}
}As soon as we execute the GetCommentsQuery, the GetCommentsHandler calls the execute method to get our data.
Summary#
This article introduced the concept of CQRS and implemented a straightforward example within our NestJS application. There are still more topics to cover when it comes to CQRS, such as events and sagas. Other patterns also work very well with CQRS, such as Event Sourcing. All of the above deserve separate articles, though.
Knowing the basics of CQRS, we know have yet another tool to consider when designing our architecture.