Back to blog
Feb 17, 2024
5 min read
Muhammad Waqar Ilyas

Building Modular, Event-Driven Microservices with CQRS in NestJS

Learn how to build scalable and maintainable microservices in NestJS with a modular architecture, CQRS, and Redis for event-driven communication. Includes a full working example.

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:

  1. Users can create tasks.
  2. Users can fetch tasks based on filters (e.g., status, priority).
  3. When a task is created, an event is emitted to notify other services (e.g., an analytics service).
  4. 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:

  1. TasksModule: Manages the creation and retrieval of tasks.
  2. 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.