Creating a Todo API using NestJS and CQRS Pattern
This is a software architecture pattern that separates read (query) operations from update (command) operations in a system, allowing these two parts to be optimized, modeled, and scaled independently.
The central idea of CQRS is that it is useful to separate the responsibilities of reading and writing in a system, given that they have different needs. For example, reads are typically straightforward and can be highly optimized using techniques like caching, while writes tend to be more complex, involving transactions, validations, and business operations.
When applying the CQRS pattern, it is possible to have read models that are optimized for data presentation and write models optimized for data update. This results in greater flexibility and performance, as well as making maintenance and evolution of the system easier.
Let’s go to a practical example: consider an e-commerce application. In a scenario like this, it is common for read operations (such as viewing products, seeing details of a product, searching for products) to be much more frequent than write operations (such as adding a new product, updating a product’s information). By applying CQRS, we could optimize the performance of reads (perhaps using caching, for example), without affecting the complexity of write operations.
It’s important to note that CQRS is not a pattern that should be applied indiscriminately. It adds complexity to the system and is more suitable for situations where there is a clear discrepancy between read and write operations or where performance is a major concern.
In this article, we will explore how to create a Todo API using NestJS and the Command Query Responsibility Segregation (CQRS) design pattern.
Let’s get started!
Initial Setup
First, install NestJS globally using npm:
npm i -g @nestjs/cli
Next, create a new project:
nest new todo-app
Now, inside your new project, install the necessary dependencies:
npm install @nestjs/cqrs typeorm sqlite3
We will use SQLite as a database, but you can choose any supported by TypeORM.
Creating the Task Entity
The first thing we will do is define the Task entity. This will represent a task within our application. Our Task entity will look like this:
// ./task/entities/task.entity
import { Column, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn, Entity } from 'typeorm';
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
description: string;
@Column({ default: false })
completed: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Here, description
is the description of the task, completed
is a flag that indicates whether the task has been completed or not, and createdAt
and updatedAt
are automatically generated timestamps.
CQRS and the creation of commands and queries
With the Task entity defined, we now focus on CQRS. CQRS is a design pattern that separates reading and updating a database into two different interfaces. This is particularly useful for large applications where you want to optimize data reading separately from data updating.
Let’s start by creating our commands. Each command represents an intention to change the state of our application in some way. For our task application, we have three commands: CreateTaskCommand
, DeleteTaskCommand
, and UpdateByCompletedCommand
.
// ./task/cqrs/commands/create-task.command
export class CreateTaskCommand {
constructor(public readonly description: string) {}
}
// ./task/cqrs/commands/delete-task.command
export class DeleteTaskCommand {
id: number;
constructor(id: number) {
this.id = id;
}
}
// ./task/cqrs/commands/update-by-completed.command
export class UpdateByCompletedCommand {
id: number;
completed: boolean;
constructor(id: number, completed: boolean) {
this.id = id;
this.completed = completed === "true"? true : false;
}
}
Next, let’s create our queries. Queries represent the intention to fetch some kind of information from our application. For our task application, we have two queries: ListTaskQuery
and ListByIdTaskQuery
.
//./task/cqrs/queries/list-task.query.ts
export class ListTaskQuery {}
// ./task/cqrs/queries/list-by-id-task.query
export class ListByIdTaskQuery {
id: number;
constructor(id: number) {
this.id = id;
}
}
Creating the Handlers
Now that we have our commands and queries, we need something to handle them. This is what the handlers do. Each command and query has its own handler.
// ./task/cqrs/handlers/create-task.handdler.ts
@CommandHandler(CreateTaskCommand)
export class CreateTaskHandler implements ICommandHandler<CreateTaskCommand> {
constructor(
@InjectRepository(Task)
private readonly taskRepository: Repository<Task>,
) {}
execute(command: CreateTaskCommand): Promise<Task> {
const { description } = command;
return this.taskRepository.save({ description });
}
}
// ./task/cqrs/handlers/delete-task.handdler.ts
@CommandHandler(DeleteTaskCommand)
export class DeleteTaskHandler implements ICommandHandler<DeleteTaskCommand> {
constructor(
@InjectRepository(Task)
private readonly taskRepository: Repository<Task>,
) {}
async execute(command: DeleteTaskCommand): Promise<void> {
const { id } = command;
await this.taskRepository.delete(id);
}
}
// ./task/cqrs/handlers/update-by-completed.handler.ts
@CommandHandler(UpdateByCompletedCommand)
export class UpdateByCompletedHandler
implements ICommandHandler<UpdateByCompletedCommand>
{
constructor(
@InjectRepository(Task)
private readonly taskRepository: Repository<Task>,
) {}
async execute(command: UpdateByCompletedCommand): Promise<void> {
const { id, completed } = command;
await this.taskRepository.update(id, { completed });
}
}
// ./task/cqrs/handlers/list-task.handdler.ts
@QueryHandler(ListTaskQuery)
export class ListTaskHandler implements IQueryHandler<ListTaskQuery> {
constructor(
@InjectRepository(Task)
private readonly taskRepository: Repository<Task>,
) {}
execute(): Promise<Task[]> {
return this.taskRepository.find();
}
}
// ./task/cqrs/handlers/list-by-id-task.handdler.ts
@QueryHandler(ListByIdTaskQuery)
export class ListByIdTaskHandler implements IQueryHandler<ListByIdTaskQuery> {
constructor(
@InjectRepository(Task)
private readonly taskRepository: Repository<Task>,
) {}
execute(query: ListByIdTaskQuery): Promise<Task> {
const { id } = query;
return this.taskRepository.findOneBy({ id });
}
}
Creating the Controller and Module
Finally, we need to create a controller to define our API routes and a module to group everything together.
// ./task/task.controller.ts
@Controller('task')
export class TaskController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
async create(@Body('description') description: string) {
return this.commandBus.execute(new CreateTaskCommand(description));
}
@Get()
async find() {
return this.queryBus.execute(new ListTaskQuery());
}
@Get(':id')
async findById(@Param('id') id: number) {
return this.queryBus.execute(new ListByIdTaskQuery(id));
}
@Patch(':id/completed/:completed')
async updateByCompleted(
@Param('id') id: number,
@Param('completed') completed: boolean,
) {
return this.commandBus.execute(new UpdateByCompletedCommand(id, completed));
}
@Delete(':id')
async delete(@Param('id') id: number) {
return this.commandBus.execute(new DeleteTaskCommand(id));
}
}
// ./task/task.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TaskController } from './task.controller';
import { Task } from './entities/task.entity';
import { CreateTaskHandler } from './cqrs/handlers/create-task.handdler';
import { ListTaskHandler } from './cqrs/handlers/list-task.handdler';
import { ListByIdTaskHandler } from './cqrs/handlers/list-by-id-task.handdler';
import { DeleteTaskHandler } from './cqrs/handlers/delete-task.handdler';
import { UpdateByCompletedHandler } from './cqrs/handlers/update-by-completed.handler';
@Module({
imports: [TypeOrmModule.forFeature([Task]), CqrsModule],
controllers: [TaskController],
providers: [
CreateTaskHandler,
ListTaskHandler,
ListByIdTaskHandler,
DeleteTaskHandler,
UpdateByCompletedHandler,
],
})
export class TaskModule {}
// ./app.module.ts
import { Module } from '@nestjs/common';
import { TaskModule } from './task/task.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Task } from './task/entities/task.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'database.sqlite',
synchronize: true,
logging: true,
entities: [Task],
}),
TaskModule,
],
})
export class AppModule {}
And that’s it! You now have a CQRS-based Todo API using NestJS. This API allows you to create, update, delete, and list tasks. Additionally, thanks to CQRS, you can optimize data reading and updating separately if needed.
If you want to listen in portuguese, follow the link: