NestJS has emerged as a popular choice for building server-side applications, especially with its ability to embrace modularity and flexibility. In this article, we’ll dive into building an advanced event-driven microservice with CQRS (Command Query Responsibility Segregation) using NestJS. This will include:
- Modular architecture
- Event handling with Redis as a message broker
- Practical use of CQRS for separation of reads and writes
By the end, you’ll have a working example of a scalable and maintainable application.
The Problem Statement
Imagine you are tasked with building a simple task management service. The requirements are:
- Users can create tasks.
- Users can fetch tasks based on filters (e.g., status, priority).
- When a task is created, an event is emitted to notify other services (e.g., an analytics service).
- The system should handle large-scale concurrent events seamlessly.
We’ll tackle this using NestJS’s modular system, Redis for messaging, and CQRS to separate commands and queries.
Setting Up the Project
Let’s start with the setup:
npm install -g @nestjs/cli
nest new task-management-service
cd task-management-service
npm install @nestjs/cqrs @nestjs/microservices redis ioredis class-validator
Defining the Modules
We will create two modules:
- TasksModule: Manages the creation and retrieval of tasks.
- EventsModule: Handles emitting and listening to events via Redis.
Tasks Module
Entity
Define the Task
entity:
// src/tasks/entities/task.entity.ts
export class Task {
id: string;
title: string;
description: string;
status: "PENDING" | "COMPLETED";
priority: "LOW" | "MEDIUM" | "HIGH";
createdAt: Date;
}
Command
Create a command to handle task creation:
// src/tasks/commands/create-task.command.ts
export class CreateTaskCommand {
constructor(
public readonly title: string,
public readonly description: string,
public readonly priority: "LOW" | "MEDIUM" | "HIGH"
) {}
}
Command Handler
The command handler contains the business logic for creating tasks:
// src/tasks/handlers/create-task.handler.ts
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
import { CreateTaskCommand } from "../commands/create-task.command";
import { Task } from "../entities/task.entity";
import { EventsService } from "../../events/events.service";
import { v4 as uuidv4 } from "uuid";
@CommandHandler(CreateTaskCommand)
export class CreateTaskHandler implements ICommandHandler<CreateTaskCommand> {
private tasks: Task[] = [];
constructor(private readonly eventsService: EventsService) {}
async execute(command: CreateTaskCommand): Promise<Task> {
const { title, description, priority } = command;
const newTask: Task = {
id: uuidv4(),
title,
description,
status: "PENDING",
priority,
createdAt: new Date(),
};
this.tasks.push(newTask);
// Emit task creation event
await this.eventsService.emit("task.created", newTask);
return newTask;
}
}
Query
Define a query to fetch tasks:
// src/tasks/queries/get-tasks.query.ts
export class GetTasksQuery {
constructor(public readonly status?: "PENDING" | "COMPLETED") {}
}
Query Handler
// src/tasks/handlers/get-tasks.handler.ts
import { IQueryHandler, QueryHandler } from "@nestjs/cqrs";
import { GetTasksQuery } from "../queries/get-tasks.query";
import { Task } from "../entities/task.entity";
@QueryHandler(GetTasksQuery)
export class GetTasksHandler implements IQueryHandler<GetTasksQuery> {
private tasks: Task[] = [];
async execute(query: GetTasksQuery): Promise<Task[]> {
if (query.status) {
return this.tasks.filter((task) => task.status === query.status);
}
return this.tasks;
}
}
Module Setup
// src/tasks/tasks.module.ts
import { Module } from "@nestjs/common";
import { CqrsModule } from "@nestjs/cqrs";
import { CreateTaskHandler } from "./handlers/create-task.handler";
import { GetTasksHandler } from "./handlers/get-tasks.handler";
import { EventsModule } from "../events/events.module";
@Module({
imports: [CqrsModule, EventsModule],
providers: [CreateTaskHandler, GetTasksHandler],
})
export class TasksModule {}
Events Module
This module handles publishing and subscribing to events.
Service
// src/events/events.service.ts
import { Injectable } from "@nestjs/common";
import { Redis } from "ioredis";
@Injectable()
export class EventsService {
private readonly redisClient: Redis;
constructor() {
this.redisClient = new Redis();
}
async emit(event: string, payload: any): Promise<void> {
await this.redisClient.publish(event, JSON.stringify(payload));
}
subscribe(event: string, callback: (payload: any) => void): void {
const subscriber = new Redis();
subscriber.subscribe(event);
subscriber.on("message", (channel, message) => {
if (channel === event) {
callback(JSON.parse(message));
}
});
}
}
Module Setup
// src/events/events.module.ts
import { Module } from "@nestjs/common";
import { EventsService } from "./events.service";
@Module({
providers: [EventsService],
exports: [EventsService],
})
export class EventsModule {}
Bootstrap the Application
Add the modules to the main application module:
// src/app.module.ts
import { Module } from "@nestjs/common";
import { TasksModule } from "./tasks/tasks.module";
import { EventsModule } from "./events/events.module";
@Module({
imports: [TasksModule, EventsModule],
})
export class AppModule {}
Testing the Application
Creating a Task
Use a REST client like Postman or Curl to send a POST request:
POST http://localhost:3000/tasks
{
"title": "Finish article",
"description": "Complete the advanced NestJS article.",
"priority": "HIGH"
}
Subscribing to Events
Run a Redis client to listen for events:
redis-cli
subscribe task.created
You should see the task details whenever a new task is created.
Conclusion
This project showcases how NestJS’s modular architecture, coupled with CQRS and event-driven patterns, can help you build scalable and maintainable microservices. By separating command and query responsibilities and leveraging Redis for event handling, we’ve created a system ready to scale in a real-world production environment.