- 1. API with NestJS #1. Controllers, routing and the module structure
- 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
- 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
- 4. API with NestJS #4. Error handling and data validation
- 5. API with NestJS #5. Serializing the response with interceptors
- 6. API with NestJS #6. Looking into dependency injection and modules
- 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
- 8. API with NestJS #8. Writing unit tests
- 9. API with NestJS #9. Testing services and controllers with integration tests
- 10. API with NestJS #10. Uploading public files to Amazon S3
- 11. API with NestJS #11. Managing private files with Amazon S3
- 12. API with NestJS #12. Introduction to Elasticsearch
- 13. API with NestJS #13. Implementing refresh tokens using JWT
- 14. API with NestJS #14. Improving performance of our Postgres database with indexes
- 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
- 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
- 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
- 18. API with NestJS #18. Exploring the idea of microservices
- 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
- 20. API with NestJS #20. Communicating with microservices using the gRPC framework
- 21. API with NestJS #21. An introduction to CQRS
- 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
- 23. API with NestJS #23. Implementing in-memory cache to increase the performance
- 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
- 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
- 26. API with NestJS #26. Real-time chat with WebSockets
- 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
- 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
- 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
- 30. API with NestJS #30. Scalar types in GraphQL
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.
1 | npm 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
1 2 3 4 5 6 7 8 9 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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; } } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
1 2 3 4 5 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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.
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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.