Complete Guide to CQRS Pattern in NestJS
When developing backend systems that require complex business logic and high performance, the traditional CRUD pattern often shows its limitations. In such cases, the CQRS (Command Query Responsibility Segregation) pattern can be a viable solution. This guide will walk you through implementing the CQRS pattern in NestJS step-by-step and share practical insights for applying it in real-world projects.
From the core concepts of the CQRS pattern to its actual implementation and testing strategies in NestJS, this guide covers everything comprehensively to help you elevate your application architecture.
What is the CQRS Pattern? How it Differs from Traditional CRUD
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates the responsibilities of commands (writes) and queries (reads). This pattern enables complete separation of read and write operations into distinct models, allowing each to be optimized independently.
Limitations of the Traditional CRUD Approach
In a typical CRUD architecture, one service handles all operations:
@Injectable()
export class UserService {
createUser(userData: CreateUserDto) { /* ... */ }
getUser(id: string) { /* ... */ }
updateUser(id: string, userData: UpdateUserDto) { /* ... */ }
deleteUser(id: string) { /* ... */ }
}
While this structure works for small projects, it leads to several issues as business logic becomes more complex:
- Entangled business logic: Read and write logic are mixed in a single service
- Scalability limitations: Read and write operations are bundled despite different performance requirements
- Testing complexity: Having all features in one class makes unit testing difficult
How CQRS Solves These Problems
CQRS addresses these issues as follows:
- Separation of responsibilities: Clearly separates state-changing operations (Commands) from data retrieval (Queries)
- Independent scalability: Read and write operations can be optimized and scaled independently
- Clear flow: Clarifies what actions need to be taken
- Ease of testing: Each handler is independent, making unit testing easier
Setting Up CQRS Module in NestJS
To implement the CQRS pattern in NestJS, use the @nestjs/cqrs package. This package provides key components like CommandBus, QueryBus, and EventBus essential for CQRS.
Project Setup
First, install the required package:
npm install --save @nestjs/cqrs
Next, register the CqrsModule in your root module:
// app.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { UserModule } from './user/user.module';
@Module({
imports: [CqrsModule.forRoot(),UserModule,
],
})
export class AppModule {}
CqrsModule automatically injects CommandBus, QueryBus, and EventBus, laying the foundation for the CQRS pattern.
Implementing Command and Query
The core of CQRS is the clear separation between Commands and Queries.
- Command → Operations that change state (Create, Update, Delete)
- Query → Operations that read data (Read)
Here are some examples:
// commands/create-user.command.ts
export class CreateUserCommand {
constructor(public readonly name: string,public readonly email: string,
) {}
}
// queries/get-user.query.ts
export class GetUserQuery {
constructor(public readonly id: string) {}
}
Implementing Command & Query Handlers
Handlers are the core components that execute the actual logic.
// commands/handlers/create-user.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateUserCommand } from '../create-user.command';
import { UserService } from '../../user.service';
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(private readonly userService: UserService) {}
async execute(command: CreateUserCommand): Promise<any> {const { name, email } = command;return this.userService.create({ name, email });
}
}
// queries/handlers/get-user.handler.ts
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { GetUserQuery } from '../get-user.query';
import { UserService } from '../../user.service';
@QueryHandler(GetUserQuery)
export class GetUserHandler implements IQueryHandler<GetUserQuery> {
constructor(private readonly userService: UserService) {}
async execute(query: GetUserQuery): Promise<any> {return this.userService.findById(query.id);
}
}
Each handler should adhere to the Single Responsibility Principle (SRP) and handle only one Command or Query.
Applying CQRS in the Controller
Controllers delegate requests to the appropriate handler via CommandBus and QueryBus.
@Controller('users')
export class UserController {
constructor(private readonly commandBus: CommandBus,private readonly queryBus: QueryBus,
) {}
@Post()
async createUser(@Body() userData: { name: string; email: string }) {return this.commandBus.execute( new CreateUserCommand(userData.name, userData.email));
}
@Get(':id')
async getUser(@Param('id') id: string) {return this.queryBus.execute(new GetUserQuery(id));
}
}
In the CQRS environment, the controller acts only as the “input port”, with all logic delegated to handlers.
Event Handling and Sagas
The true power of CQRS shines when combined with an event-driven system.
// events/user-created.event.ts
export class UserCreatedEvent {
constructor(public readonly userId: string,public readonly email: string,
) {}
}
// events/handlers/user-created.handler.ts
@EventsHandler(UserCreatedEvent)
export class UserCreatedHandler implements IEventHandler<UserCreatedEvent> {
handle(event: UserCreatedEvent) {console.log(`User created: ${event.userId}`);
}
}
Events are triggered as a result of executing Commands/Queries and can be responded to by other parts of the system.
Testing Your CQRS Implementation
CQRS makes it very easy to test handlers individually.
describe('CreateUserHandler', () => {
it('should correctly call userService.create', async () => {const command = new CreateUserCommand('Alice', 'alice@email.com');const result = await handler.execute(command);expect(result).toBeDefined();
});
});
Each handler is an independent unit, making it easy to mock external dependencies and run tests.
CQRS Best Practices
- Always separate Commands and Queries
- Leverage EventBus to build asynchronous flows
- Make handler-level testing a habit
- Minimize business logic inside handlers: delegate to service layers
CQRS can be overkill for small projects. It's best suited for environments with complex business logic, event-driven architecture, or high traffic.