Nest.js Tutorial

An introduction to CQRS

Marcin Wanago
JavaScriptNestJSTypeScript

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.

1npm install --save @nestjs/cqrs

Let’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
1import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
2import User from '../users/user.entity';
3import Post from '../posts/post.entity';
4 
5@Entity()
6class Comment {
7  @PrimaryGeneratedColumn()
8  public id: number;
9 
10  @Column()
11  public content: string;
12 
13  @ManyToOne(() => Post, (post: Post) => post.comments)
14  public post: Post;
15 
16  @ManyToOne(() => User, (author: User) => author.posts)
17  public author: User;
18}
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
1import { IsString, IsNotEmpty, ValidateNested } from 'class-validator';
2import { Type } from 'class-transformer';
3import ObjectWithIdDTO from 'src/utils/types/objectWithId.dto';
4 
5export class CreateCommentDto {
6  @IsString()
7  @IsNotEmpty()
8  content: string;
9 
10  @ValidateNested()
11  @Type(() => ObjectWithIdDTO)
12  post: ObjectWithIdDTO;
13}
14 
15export 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
1import CreateCommentDto from '../../dto/createComment.dto';
2import User from '../../../users/user.entity';
3 
4export class CreateCommentCommand {
5  constructor(
6    public readonly comment: CreateCommentDto,
7    public readonly author: User,
8  ) {}
9}

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
1import {
2  Body,
3  ClassSerializerInterceptor,
4  Controller,
5  Post,
6  Req,
7  UseGuards,
8  UseInterceptors,
9} from '@nestjs/common';
10import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
11import RequestWithUser from '../authentication/requestWithUser.interface';
12import CreateCommentDto from './dto/createComment.dto';
13import { CommandBus } from '@nestjs/cqrs';
14import { CreateCommentCommand } from './commands/implementations/createComment.command';
15 
16@Controller('comments')
17@UseInterceptors(ClassSerializerInterceptor)
18export default class CommentsController {
19  constructor(private commandBus: CommandBus) {}
20 
21  @Post()
22  @UseGuards(JwtAuthenticationGuard)
23  async createComment(@Body() comment: CreateCommentDto, @Req() req: RequestWithUser) {
24    const user = req.user;
25    return this.commandBus.execute(
26      new CreateCommentCommand(comment, user)
27    )
28  }
29}
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
1import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
2import { CreateCommentCommand } from '../implementations/createComment.command';
3import { InjectRepository } from '@nestjs/typeorm';
4import Comment from '../../comment.entity';
5import { Repository } from 'typeorm';
6 
7@CommandHandler(CreateCommentCommand)
8export class CreateCommentHandler implements ICommandHandler<CreateCommentCommand> {
9  constructor(
10    @InjectRepository(Comment)
11    private commentsRepository: Repository<Comment>,
12  ) {}
13 
14  async execute(command: CreateCommentCommand) {
15    const newPost = await this.commentsRepository.create({
16      ...command.comment,
17      author: command.author
18    });
19    await this.commentsRepository.save(newPost);
20    return newPost;
21  }
22}
In this handler we use a repository provided by TypeORM. If you want to explore this concept more, check out API with NestJS #2. Setting up a PostgreSQL database with TypeORM

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
1import { Module } from '@nestjs/common';
2import { TypeOrmModule } from '@nestjs/typeorm';
3import Comment from './comment.entity';
4import CommentsController from './comments.controller';
5import { CqrsModule } from '@nestjs/cqrs';
6import { CreateCommentHandler } from './commands/handlers/create-comment.handler';
7 
8@Module({
9  imports: [TypeOrmModule.forFeature([Comment]), CqrsModule],
10  controllers: [CommentsController],
11  providers: [CreateCommentHandler],
12})
13export 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
1export class GetCommentsQuery {
2  constructor(
3    public readonly postId?: number,
4  ) {}
5}

To execute a query, we need an instance of the QueryBus. It acts in a very similar way to the CommandBus.

comments.controller.ts
1import {
2  Body,
3  ClassSerializerInterceptor,
4  Controller, Get,
5  Post, Query,
6  Req,
7  UseGuards,
8  UseInterceptors,
9} from '@nestjs/common';
10import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
11import RequestWithUser from '../authentication/requestWithUser.interface';
12import CreateCommentDto from './dto/createComment.dto';
13import { CommandBus, QueryBus } from '@nestjs/cqrs';
14import { CreateCommentCommand } from './commands/implementations/createComment.command';
15import { GetCommentsQuery } from './queries/implementations/getComments.query';
16import GetCommentsDto from './dto/getComments.dto';
17 
18@Controller('comments')
19@UseInterceptors(ClassSerializerInterceptor)
20export default class CommentsController {
21  constructor(
22    private commandBus: CommandBus,
23    private queryBus: QueryBus,
24  ) {}
25 
26  @Post()
27  @UseGuards(JwtAuthenticationGuard)
28  async createComment(@Body() comment: CreateCommentDto, @Req() req: RequestWithUser) {
29    const user = req.user;
30    return this.commandBus.execute(
31      new CreateCommentCommand(comment, user)
32    )
33  }
34 
35  @Get()
36  async getComments(
37    @Query() { postId }: GetCommentsDto,
38  ) {
39    return this.queryBus.execute(
40      new GetCommentsQuery(postId)
41    )
42  }
43}

When we execute the query, the query handler picks it up.

Above, we’ve also created the GetCommentsDto that can transform the postId from a string to a number. If you want to know more about serialization, look into API with NestJS #5. Serializing the response with interceptors
getComments.handler.ts
1import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
2import { GetCommentsQuery } from '../implementations/getComments.query';
3import { InjectRepository } from '@nestjs/typeorm';
4import Comment from '../../comment.entity';
5import { Repository } from 'typeorm';
6 
7@QueryHandler(GetCommentsQuery)
8export class GetCommentsHandler implements IQueryHandler&lt;GetCommentsQuery&gt; {
9  constructor(
10    @InjectRepository(Comment)
11    private commentsRepository: Repository&lt;Comment&gt;,
12  ) {}
13 
14  async execute(query: GetCommentsQuery) {
15    if (query.postId) {
16      return this.commentsRepository.find({
17        post: {
18          id: query.postId
19        }
20      });
21    }
22    return this.commentsRepository.find();
23  }
24}

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.