O que é CQRS e Por Que Usar
CQRS (Command Query Responsibility Segregation) é um padrão arquitetural que separa as operações de leitura (queries) das operações de escrita (commands) em seus próprios objetos e fluxos de processamento. A ideia central é simples: um comando muda o estado do sistema, enquanto uma query apenas lê dados sem efeitos colaterais. Isso permite que você escale, otimize e mantenha esses dois fluxos de forma independente.
Em aplicações TypeScript tradicionais, você provavelmente tem um controlador que recebe uma requisição, valida dados, chama um serviço que tanto lê quanto escreve, e retorna uma resposta. O problema cresce quando sua aplicação fica complexa: operações de leitura podem precisar de otimizações diferentes (cache, denormalização), enquanto operações de escrita precisam de transações e validações rígidas. CQRS resolve isso criando dois caminhos distintos, cada um otimizado para seu propósito.
Estrutura Fundamental: Commands e Queries
O que é um Command
Um command é uma intenção de mudar algo no sistema. Diferente de um método tradicional, um command é um objeto que encapsula essa intenção e seus dados necessários. Ele é imutável, fortemente tipado e descreve o que deve acontecer, não como.
// Define a estrutura de um command genérico
interface Command {
type: string;
}
// Um command concreto para criar um usuário
class CreateUserCommand implements Command {
readonly type = 'CreateUser';
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string
) {}
}
// Outro command para atualizar um usuário
class UpdateUserEmailCommand implements Command {
readonly type = 'UpdateUserEmail';
constructor(
public readonly userId: string,
public readonly newEmail: string
) {}
}
Notice que cada command é uma classe específica. Isso garante type-safety total em TypeScript. Quando você cria um CreateUserCommand, você não esquecerá nenhum campo obrigatório—o compilador não vai deixar compilar.
O que é uma Query
Uma query é um pedido para ler dados sem efeitos colaterais. Assim como commands, queries são objetos tipados que descrevem exatamente o que você quer saber.
// Estrutura genérica de uma query
interface Query {
type: string;
}
// Uma query para buscar um usuário por ID
class GetUserByIdQuery implements Query {
readonly type = 'GetUserById';
constructor(public readonly userId: string) {}
}
// Uma query para listar todos os usuários com paginação
class ListUsersQuery implements Query {
readonly type = 'ListUsers';
constructor(
public readonly page: number,
public readonly limit: number
) {}
}
// Uma query para buscar estatísticas
class GetUserStatisticsQuery implements Query {
readonly type = 'GetUserStatistics';
constructor(public readonly period: 'day' | 'week' | 'month') {}
}
A diferença semântica é clara: queries retornam dados, commands não. Se você quiser retornar algo de um command (como o ID gerado de um novo usuário), você deve pensar se aquilo realmente é um efeito colateral do comando ou uma query separada.
Handlers Tipados: A Máquina de Executar
Command Handlers
Um command handler é responsável por executar a lógica do comando. Ele recebe o command, valida se necessário, modifica o estado (banco de dados, memória, etc.) e pode retornar um resultado. Em CQRS puro, commands não retornam valores, mas em aplicações reais é comum retornar o ID do recurso criado ou um status de sucesso.
// Tipo genérico para um handler de command
interface CommandHandler<T extends Command, R = void> {
execute(command: T): Promise<R>;
}
// Interface de um repositório para persistência
interface UserRepository {
save(user: User): Promise<void>;
findById(id: string): Promise<User | null>;
}
// Um handler para CreateUserCommand
class CreateUserCommandHandler implements CommandHandler<CreateUserCommand, string> {
constructor(private userRepository: UserRepository) {}
async execute(command: CreateUserCommand): Promise<string> {
// Validação
if (!command.email.includes('@')) {
throw new Error('Email inválido');
}
// Criar o usuário
const user = new User(command.id, command.name, command.email);
// Persistir
await this.userRepository.save(user);
// Retornar o ID criado
return user.id;
}
}
// Um handler para UpdateUserEmailCommand
class UpdateUserEmailCommandHandler implements CommandHandler<UpdateUserEmailCommand> {
constructor(private userRepository: UserRepository) {}
async execute(command: UpdateUserEmailCommand): Promise<void> {
const user = await this.userRepository.findById(command.userId);
if (!user) {
throw new Error('Usuário não encontrado');
}
user.email = command.newEmail;
await this.userRepository.save(user);
}
}
Query Handlers
Um query handler recebe uma query e retorna dados. Diferente de commands, queries sempre retornam algo. Query handlers geralmente não têm efeitos colaterais e podem ser otimizados com caching.
// Tipo genérico para um handler de query
interface QueryHandler<T extends Query, R> {
execute(query: T): Promise<R>;
}
// Modelo de retorno para uma query de usuário
interface UserDTO {
id: string;
name: string;
email: string;
}
// Um handler para GetUserByIdQuery
class GetUserByIdQueryHandler implements QueryHandler<GetUserByIdQuery, UserDTO | null> {
constructor(private userRepository: UserRepository) {}
async execute(query: GetUserByIdQuery): Promise<UserDTO | null> {
const user = await this.userRepository.findById(query.userId);
if (!user) {
return null;
}
// Retornar DTO (Data Transfer Object)
return {
id: user.id,
name: user.name,
email: user.email,
};
}
}
// Um handler para ListUsersQuery
class ListUsersQueryHandler implements QueryHandler<ListUsersQuery, { users: UserDTO[]; total: number }> {
constructor(private userRepository: UserRepository) {}
async execute(query: ListUsersQuery): Promise<{ users: UserDTO[]; total: number }> {
// Aqui você poderia usar uma view denormalizada para leitura otimizada
const users = await this.userRepository.list(query.page, query.limit);
return {
users: users.map(u => ({
id: u.id,
name: u.name,
email: u.email,
})),
total: await this.userRepository.count(),
};
}
}
Bus de Commands e Queries: Orquestração Centralizada
Implementando um Bus
O bus é o coração do padrão CQRS. Ele registra os handlers e, quando você envia um command ou query, ele encontra o handler correto e o executa. Isso desacopla seu código: você não precisa saber qual handler vai executar, apenas envia a mensagem.
// Definição do bus de commands
class CommandBus {
private handlers: Map<string, CommandHandler<any, any>> = new Map();
register<T extends Command, R = void>(
commandType: string,
handler: CommandHandler<T, R>
): void {
this.handlers.set(commandType, handler);
}
async execute<T extends Command, R = void>(command: T): Promise<R> {
const handler = this.handlers.get(command.type);
if (!handler) {
throw new Error(`Nenhum handler registrado para ${command.type}`);
}
return handler.execute(command);
}
}
// Definição do bus de queries
class QueryBus {
private handlers: Map<string, QueryHandler<any, any>> = new Map();
register<T extends Query, R>(
queryType: string,
handler: QueryHandler<T, R>
): void {
this.handlers.set(queryType, handler);
}
async execute<T extends Query, R>(query: T): Promise<R> {
const handler = this.handlers.get(query.type);
if (!handler) {
throw new Error(`Nenhum handler registrado para ${query.type}`);
}
return handler.execute(query);
}
}
Exemplo de Uso Prático
// Simular um repositório em memória para exemplo
class InMemoryUserRepository implements UserRepository {
private users: Map<string, User> = new Map();
async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async list(page: number, limit: number): Promise<User[]> {
return Array.from(this.users.values()).slice(
(page - 1) * limit,
page * limit
);
}
async count(): Promise<number> {
return this.users.size;
}
}
// Classe User simples
class User {
constructor(
public id: string,
public name: string,
public email: string
) {}
}
// Configuração e execução
async function main() {
const userRepository = new InMemoryUserRepository();
// Criar os buses
const commandBus = new CommandBus();
const queryBus = new QueryBus();
// Registrar handlers
commandBus.register('CreateUser', new CreateUserCommandHandler(userRepository));
commandBus.register('UpdateUserEmail', new UpdateUserEmailCommandHandler(userRepository));
queryBus.register('GetUserById', new GetUserByIdQueryHandler(userRepository));
queryBus.register('ListUsers', new ListUsersQueryHandler(userRepository));
// Executar um command
const userId = await commandBus.execute(
new CreateUserCommand('user-1', 'João Silva', 'joao@example.com')
);
console.log('Usuário criado:', userId);
// Executar uma query
const user = await queryBus.execute(
new GetUserByIdQuery(userId)
);
console.log('Usuário encontrado:', user);
// Atualizar email
await commandBus.execute(
new UpdateUserEmailCommand(userId, 'joao.novo@example.com')
);
console.log('Email atualizado');
// Listar usuários
const list = await queryBus.execute(
new ListUsersQuery(1, 10)
);
console.log('Lista de usuários:', list);
}
main().catch(console.error);
Integração com Express e Tipagem de Ponta a Ponta
Controladores Tipados
Agora que temos o CQRS implementado, vamos integrar com um framework web como Express mantendo a tipagem forte.
import express, { Request, Response } from 'express';
interface CreateUserRequest {
name: string;
email: string;
}
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
// Middleware de validação de request
function validateCreateUserRequest(
req: Request,
res: Response,
next: Function
): void {
const { name, email } = req.body as unknown;
if (!name || typeof name !== 'string') {
res.status(400).json({ success: false, error: 'Nome é obrigatório' });
return;
}
if (!email || typeof email !== 'string') {
res.status(400).json({ success: false, error: 'Email é obrigatório' });
return;
}
next();
}
// Controlador de usuários
class UserController {
constructor(
private commandBus: CommandBus,
private queryBus: QueryBus
) {}
async createUser(req: Request, res: Response): Promise<void> {
try {
const { name, email } = req.body as CreateUserRequest;
const userId = await this.commandBus.execute(
new CreateUserCommand(
`user-${Date.now()}`,
name,
email
)
);
const response: ApiResponse<{ id: string }> = {
success: true,
data: { id: userId },
};
res.status(201).json(response);
} catch (error) {
const response: ApiResponse<null> = {
success: false,
error: error instanceof Error ? error.message : 'Erro desconhecido',
};
res.status(400).json(response);
}
}
async getUser(req: Request, res: Response): Promise<void> {
try {
const { userId } = req.params;
const user = await this.queryBus.execute(
new GetUserByIdQuery(userId)
);
if (!user) {
const response: ApiResponse<null> = {
success: false,
error: 'Usuário não encontrado',
};
res.status(404).json(response);
return;
}
const response: ApiResponse<UserDTO> = {
success: true,
data: user,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
success: false,
error: error instanceof Error ? error.message : 'Erro desconhecido',
};
res.status(400).json(response);
}
}
async listUsers(req: Request, res: Response): Promise<void> {
try {
const page = Math.max(1, parseInt(req.query.page as string) || 1);
const limit = Math.min(100, parseInt(req.query.limit as string) || 10);
const result = await this.queryBus.execute(
new ListUsersQuery(page, limit)
);
const response: ApiResponse<typeof result> = {
success: true,
data: result,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
success: false,
error: error instanceof Error ? error.message : 'Erro desconhecido',
};
res.status(400).json(response);
}
}
}
// Setup do Express
function setupRoutes(
app: express.Application,
userRepository: UserRepository,
commandBus: CommandBus,
queryBus: QueryBus
): void {
const controller = new UserController(commandBus, queryBus);
app.post('/users', validateCreateUserRequest, (req, res) =>
controller.createUser(req, res)
);
app.get('/users/:userId', (req, res) =>
controller.getUser(req, res)
);
app.get('/users', (req, res) =>
controller.listUsers(req, res)
);
}
// Inicializar aplicação
const app = express();
app.use(express.json());
const userRepository = new InMemoryUserRepository();
const commandBus = new CommandBus();
const queryBus = new QueryBus();
commandBus.register('CreateUser', new CreateUserCommandHandler(userRepository));
commandBus.register('UpdateUserEmail', new UpdateUserEmailCommandHandler(userRepository));
queryBus.register('GetUserById', new GetUserByIdQueryHandler(userRepository));
queryBus.register('ListUsers', new ListUsersQueryHandler(userRepository));
setupRoutes(app, userRepository, commandBus, queryBus);
app.listen(3000, () => {
console.log('Servidor rodando em http://localhost:3000');
});
Padrões Avançados e Boas Práticas
Event Sourcing e Auditoria
Um dos maiores benefícios do CQRS é a possibilidade de integração com Event Sourcing. Cada comando que altera o estado pode gerar um evento que fica registrado permanentemente, criando um histórico completo de mudanças.
// Evento de domínio
interface DomainEvent {
type: string;
timestamp: Date;
aggregateId: string;
}
class UserCreatedEvent implements DomainEvent {
type = 'UserCreated';
timestamp = new Date();
constructor(
public aggregateId: string,
public name: string,
public email: string
) {}
}
class UserEmailUpdatedEvent implements DomainEvent {
type = 'UserEmailUpdated';
timestamp = new Date();
constructor(
public aggregateId: string,
public newEmail: string
) {}
}
// Handler que publica eventos
class CreateUserCommandHandlerWithEvents implements CommandHandler<CreateUserCommand, string> {
private events: DomainEvent[] = [];
constructor(
private userRepository: UserRepository,
private eventBus: EventBus
) {}
async execute(command: CreateUserCommand): Promise<string> {
if (!command.email.includes('@')) {
throw new Error('Email inválido');
}
const user = new User(command.id, command.name, command.email);
await this.userRepository.save(user);
// Publicar evento
const event = new UserCreatedEvent(user.id, user.name, user.email);
await this.eventBus.publish(event);
return user.id;
}
}
// Bus de eventos para handlers de eventos
interface EventHandler<T extends DomainEvent> {
handle(event: T): Promise<void>;
}
class EventBus {
private handlers: Map<string, EventHandler<any>[]> = new Map();
subscribe<T extends DomainEvent>(
eventType: string,
handler: EventHandler<T>
): void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, []);
}
this.handlers.get(eventType)!.push(handler);
}
async publish(event: DomainEvent): Promise<void> {
const handlers = this.handlers.get(event.type) || [];
await Promise.all(
handlers.map(handler => handler.handle(event))
);
}
}
// Um handler de evento que envia email
class SendWelcomeEmailEventHandler implements EventHandler<UserCreatedEvent> {
async handle(event: UserCreatedEvent): Promise<void> {
console.log(`Enviando email de boas-vindas para ${event.email}`);
// Lógica real de envio de email aqui
}
}
Validação e Segurança
Commands devem ser validados antes de serem executados. Uma abordagem é criar um middleware de validação genérico.
interface Validator<T> {
validate(object: T): Promise<string[]>; // Retorna array de erros
}
class EmailValidator implements Validator<CreateUserCommand> {
async validate(command: CreateUserCommand): Promise<string[]> {
const errors: string[] = [];
if (!command.email.includes('@')) {
errors.push('Email deve conter @');
}
if (command.email.length < 5) {
errors.push('Email muito curto');
}
return errors;
}
}
class ValidatingCommandBus {
private handlers: Map<string, CommandHandler<any, any>> = new Map();
private validators: Map<string, Validator<any>[]> = new Map();
register<T extends Command, R = void>(
commandType: string,
handler: CommandHandler<T, R>
): void {
this.handlers.set(commandType, handler);
}
registerValidator<T extends Command>(
commandType: string,
validator: Validator<T>
): void {
if (!this.validators.has(commandType)) {
this.validators.set(commandType, []);
}
this.validators.get(commandType)!.push(validator);
}
async execute<T extends Command, R = void>(command: T): Promise<R> {
const validators = this.validators.get(command.type) || [];
const allErrors: string[] = [];
for (const validator of validators) {
const errors = await validator.validate(command);
allErrors.push(...errors);
}
if (allErrors.length > 0) {
throw new Error(`Validação falhou: ${allErrors.join(', ')}`);
}
const handler = this.handlers.get(command.type);
if (!handler) {
throw new Error(`Nenhum handler registrado para ${command.type}`);
}
return handler.execute(command);
}
}
Conclusão
Durante este artigo, você aprendeu que CQRS é uma separação deliberada entre leitura e escrita que permite otimizar cada fluxo independentemente. Em TypeScript, isso se traduz em commands e queries fortemente tipados, evitando bugs em tempo de compilação. Você também viu que o bus (command e query) é o padrão central que desacopla produtores de consumidores, tornando o código mais testável e manutenível. Por fim, implementamos uma integração prática com Express, mostrando que CQRS não é apenas teoria—é um padrão aplicável imediatamente em aplicações reais, especialmente quando escalabilidade e manutenção de longo prazo são prioridades.