Criação de uma API de Tarefas (Todo) com NestJS e CQRS Pattern
O CQRS é um padrão de arquitetura de software que separa as operações de leitura (query) das operações de atualização (command) de um sistema, possibilitando que essas duas partes sejam otimizadas, modeladas e escaladas de maneira independente.
A ideia central do CQRS é que é útil separar as responsabilidades de leitura e gravação em um sistema, dado que elas têm diferentes necessidades. Por exemplo, leituras normalmente são simples e podem ser altamente otimizadas através de técnicas como caching, enquanto as gravações tendem a ser mais complexas, envolvendo transações, validações e operações de negócios.
Ao aplicar o padrão CQRS, é possível ter modelos de leitura que são otimizados para a apresentação de dados e modelos de gravação otimizados para atualização de dados. Isso resulta em maior flexibilidade e performance, além de facilitar a manutenção e a evolução do sistema.
Vamos a um exemplo prático: considere uma aplicação de e-commerce. Em um cenário como esse, é comum que as operações de leitura (como visualizar produtos, ver detalhes de um produto, pesquisar produtos) sejam muito mais frequentes do que as operações de gravação (como adicionar um novo produto, atualizar as informações de um produto). Ao aplicar o CQRS, poderíamos otimizar o desempenho das leituras (talvez utilizando um cache, por exemplo), sem afetar a complexidade das operações de gravação.
É importante notar que CQRS não é um padrão que deve ser aplicado indiscriminadamente. Ele adiciona complexidade ao sistema e é mais adequado para situações onde existe uma clara discrepância entre as operações de leitura e gravação ou onde a performance é uma grande preocupação.
Neste artigo, exploraremos como criar uma API de tarefas (Todo) usando NestJS e o padrão de design CQRS (Command Query Responsibility Segregation).
Vamos começar!
Configuração inicial
Primeiramente, instale NestJS globalmente usando npm:
npm i -g @nestjs/cli
Após isso, crie um novo projeto:
nest new todo-app
Agora, dentro do seu novo projeto, instale as dependências necessárias:
npm install @nestjs/cqrs typeorm sqlite3
Vamos utilizar o SQLite como banco de dados, mas você pode escolher qualquer um suportado pelo TypeORM.
Criando a entidade Task
A primeira coisa que faremos é definir a entidade Task. Isso irá representar uma tarefa dentro da nossa aplicação. Nossa entidade Task será assim:
// ./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;
}
Aqui, description
é a descrição da tarefa, completed
é uma flag que indica se a tarefa foi concluída ou não, e createdAt
e updatedAt
são timestamps gerados automaticamente.
CQRS e a criação das commands e queries
Com a entidade Task definida, vamos agora focar no CQRS. CQRS é um padrão de design que separa a leitura e a atualização de um banco de dados em duas interfaces diferentes. Isso é particularmente útil para grandes aplicações onde você quer otimizar a leitura de dados separadamente da atualização dos dados.
Vamos começar criando nossos comandos. Cada comando representa uma intenção de alterar o estado do nosso aplicativo de alguma forma. Para nossa aplicação de tarefas, temos três comandos: CreateTaskCommand
, DeleteTaskCommand
e 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;
}
}
Em seguida, vamos criar nossas queries. As queries representam a intenção de buscar algum tipo de informação do nosso aplicativo. Para nossa aplicação de tarefas, temos duas queries: ListTaskQuery
e 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;
}
}
Criando os Handlers
Agora que temos nossos comandos e queries, precisamos de algo para lidar com eles. Isso é o que os handlers fazem. Cada comando e query tem seu próprio 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 });
}
}
Criando o Controller e o Module
Por último, precisamos criar um controlador para definir as rotas da nossa API e um módulo para agrupar tudo isso.
// ./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 {}
E isso é tudo! Agora você tem uma API de tarefas baseada em CQRS usando NestJS. Essa API permite que você crie, atualize, delete e liste tarefas. Além disso, graças ao CQRS, você pode otimizar a leitura e a atualização de dados separadamente se precisar.
Caso queira ler em Inglês, clique no link:
Creating a Todo API using NestJS and CQRS Pattern | by Cláudio Rapôso | Jun, 2023 | Medium