Contact us

🌍 All

About us

Digitalization

News

Startups

Development

Design

Introduction to CQRS with NestJS

Michał Cywiński

Mar 03, 202317 min read

Node.jsBack-end development

Table of Content

  • Let's define the problem

  • CQRS by definition

  • CQRS by example

    • UserService before refactoring

    • Problems with the existing UserService

    • CreateUserCommand

    • SendActivationEmailEventHandler

    • GetActiveUsersQuery

    • NestJS controllers

  • Introducing CQRS into existing codebases

  • Final thoughts

Let's define the problem

For many years, most applications have been built using classic N-layer architecture (see below). However, even today, when development teams stick to best practices such as the use of design patterns and the application of solid principles and unit testing services, the approach very often leads to overly complex codebases that are difficult to maintain. We often call such a codebase a "Big Ball of Mud".

At this point you might ask: “Why does this Big Ball of Mud develop despite my team and I having followed state-of-the-art practices? And why does it happen in most projects I participate in?” Although we may initially adhere to said principles which seemingly keep us on track, still, in time those layers of code become increasingly complex. As a result, many services end up referencing each other and are rendered too tedious to test.

This is where and why microservices first came into play. It’s also when everyone got hyped once it started to look as if a silver bullet had been found for the problems mentioned above. After some time, however, many teams still found themselves encountering the very same problems, only much harder to solve: “a distributed Big Ball of Mud". But not just the same problems as before: now a team also had to deal with communication and data synchronization issues – not to mention multiple deployment units that needed handling. A veritable orchestra of problems to be harmonized, and one which development teams continue to face to this day.

So, how do we save ourselves from this complexity hell? Is there any option for having a beautifully looking codebase that’s a pleasure to unit test? 

Yes. Let me introduce you to the CQRS pattern. It may not solve all problems, but in any of the teams I’ve worked for, CQRS has cured many a headache.

CQRS by definition

CQRS is an acronym for Command Query Responsibility Segregation. But what does it really mean? The CQRS pattern assumes that a programmer will use three main types of requesting classes to trigger and orchestrate business logic:

Commands that are responsible for altering state-of-application data; 

Queries that are used solely for retrieving application data without mutating the application state;

Events that can be raised throughout business logic execution. For instance, ones that may trigger some additional processing of data after the execution of a command.

Each type of requesting class listed above has a corresponding handler that executes the actual business logic. Think of a handler as a single service method. Requesting classes such as commands only carry input (payload) information for a given handler.

What is also important is that commands and queries always have one corresponding handler. Events can have multiple handlers. For example, we can notify some event by e-mail and SMS, giving us two different handlers for each notification type, yet still reacting to the same event.

Each type of requesting class is always published on a corresponding bus: Command bus, Query bus or Event bus. Those buses are aware of registered handlers (usually via dependency injection container) and know what handler must be executed with the provided instance of requesting class.

But what does it really mean? Enough theory, then – let's see it in practice.

CQRS by example

In the following example we will explore CQRS by refactoring a piece of application service into commands, queries, events and their corresponding handlers.

UserService before refactoring

Below is a simple UserService class responsible for creating a user and getting active users.

Creation logic is responsible for creating a user with a hashed password and dispatching an activation e-mail. When a user is created, we are returning an ID of the newly created account.

Getting logic is responsible for filtering user accounts to get only active ones, then maps them to DTOs.

Problems with the existing UserService

Before doing the actual refactoring, we should consider what the problems are with such service classes and with this classic service approach:

What does creating a user have to do with getting active users? Aside from using the same repository for data access… nothing, really.

If we want to unit test getting active users, we need to mock or inject NotificationService that has nothing to do with getActiveUsers.

Services become messier as the project goes on. With such an approach, we eventually end up with spaghetti code.

We have been told for years that we should not put business logic code into controllers. In reality, what we are doing is moving the mess from controllers to services. As a result, this same mess resides in services instead of controllers. However, this still doesn't give us any gain in terms of maintainability of our codebase.

With that in mind, it’s time to refactor. For that purpose, we'll use the excellent package for NestJS, which also comes with great documentation.

CreateUserCommand

Introducing our first command and handler is as simple as declaring two corresponding classes for them: CreateUserCommand and CreateUserCommandHandler (the latter must be decorated by @CommandHandler, and implement the ICommandHandler interface).

In our command, we store required information to create a user account and modify the state of our application: e-mail address and password. Our handler contains the logic extracted from the original UserService class.

It is worth noting that we are injecting a new dependency: EventBus, which also comes from @nestjs/cqrs. It is used to raise an event indicating that a user account has been created. Event handlers (explained later in this post) can independently collect such events and react to them: in our case, sending an activation e-mail with a secret token.

What’s really nice is the fact that unit testing becomes super easy when we compare it with the previous approach. All we need to check is whether the user has been saved in the database with correct properties, and whether an event has been raised (which can be verified using EventBus mock). 

So, it doesn’t really matter how many handlers will react to that published event since they will be tested independently. This gives us more freedom because we don’t rely on any services such as NotificationService, which was injected in the initial UserService.  We are thus enabled to decouple from the actual creation of the user entity all logic triggered by user registration. Pretty cool, isn't it?

SendActivationEmailEventHandler

The name says it all. This is our event handler that reacts to UserCreatedEvent raised in CreateUserCommandHandler and sends an activation e-mail based on the payload provided and constructed in the event class in the mentioned command handler.

Similarly with command handlers, we must decorate it with @EventsHandler decorator and implement the IEventHandler interface from @nestjs/cqrs.

This is where the notification service is injected. Notice that we can now unit test sending an activation e-mail without actually creating a user account. Our code is now much more modular.

What’s more, we can easily imagine another event handler reacting to the same event in an independent way – i.e. by sending SMS. That handler can also be tested independently from other event handlers, and from the command handler that was raised. The more event-based our application becomes, the more we can gain from such architecture.

GetActiveUsersQuery

Last, but not least, we will introduce a refactored GetActiveUsersQuery and GetActiveUsersQueryHandler decorated by @QueryHandler decorator that should implement the IQueryHandler interface.

Query class carries all necessary information (payload) to execute logic defined in the corresponding  handler. In our case, that is the isActive property filter. After executing filtering logic, results are mapped to the DTOs in exactly the same manner as in the initial UserService.

NestJS controllers

With all queries and commands in place, we can inject QueryBus and CommandBus into our API controller. Handlers will be automatically matched and called. We get rid of any dependencies (services) besides buses. Clean, isn't it?

Introducing CQRS into existing codebases

One might say: “OK, that's cool. It was nice to see this pattern, but I'm working with that legacy codebase. I cannot introduce it to my project at this point”. That's not an argument. You can introduce this pattern to the existing codebase. Sometimes easily, sometimes with some additional effort. But I've been there and have done that in enterprise-grade codebases. And it’s always been worth it.

You don't have to introduce this pattern all at once. In fact, the CQRS pattern really shines because you can introduce it incrementally without breaking your project. New functionalities can be mostly implemented as command and query handlers right away.  Yes, an instant gain. 

Obviously, they will at times require that you inject some complicated services (which in an ideal scenario, you would not want to inject), however, you can still mock them to make unit testing – and your life – a bit easier.

For existing services, you can start refactoring by extracting handlers once you touch some service method related to your current Jira/Trello/your-favourite-issue-tracker ticket. Simply start by extracting the logic into a new handler without any modifications so that you don’t mix a new feature with tidying up. As always, remember to add unit tests if there weren't any before. 

Create a pull request, make sure everyone understands the refactored code, and merge it into your development branch. Now it’s time for the actual task. Now you should be able to add a new feature with little effort since you are operating only on the CQRS handler level. 

Yes, this is what we were initially aiming for: super-simple classes that are specialized in doing exactly one thing while using only the dependencies it requires.

Final thoughts

CQRS may require writing some additional code that carries payload for our handlers, but it makes it extremely easy to have a lot of small, maintainable classes that are easy to unit test.

Would I recommend it for every project? Probably not. If all you need is a few simple CRUDs and you want to deliver things quickly, it might be better to stick to good old service classes.  You might then want to reconsider refactoring the old code as soon as your project scope changes or its complexity increases.

In my developer toolbelt for medium-sized and larger projects, the CQRS pattern is always a way to go. It’ll pay off – I promise.

 
Introduction to CQRS with NestJS

Published on March 03, 2023

Share


Michał Cywiński Fullstack developer

Don't miss a beat - subscribe to our newsletter
I agree to receive marketing communication from Startup House. Click for the details

You may also like...

Node.js: The Backbone of Modern Web Applications
Node.jsSoftware developmentProduct development

Node.js: The Backbone of Modern Web Applications

Node.js revolutionizes web development by offering a JavaScript runtime for creating high-performance, scalable server-side applications. With its non-blocking, event-driven architecture and a robust npm ecosystem, Node.js is a go-to technology for developers building modern web applications.

Alexander Stasiak

Jun 04, 20245 min read

Mobile Backend as a Service
Back-end developmentMobile

Mobile Backend as a Service

Mobile Backend as a Service (MBaaS) streamlines mobile app development by offering cloud-based backend functionalities such as data storage, user authentication, and push notifications. This allows developers to focus on front-end features while the service provider handles the complexities of backend management. Explore how MBaaS enhances app performance, simplifies development, and reduces costs, making it an essential tool for modern mobile apps.

Marek Majdak

Aug 09, 20245 min read

What is Backend as a Service (BaaS)?
Back-end developmentDigital products

What is Backend as a Service (BaaS)?

Backend as a Service (BaaS) streamlines application development by handling backend functions such as database management, user authentication, and hosting. This allows developers to focus on front-end design and user experience, making it a popular choice for startups and small teams. Discover how BaaS can enhance your development process by reducing complexity and accelerating deployment.

Marek Majdak

Aug 02, 20245 min read

Let's talk
let's talk

Let's build

something together

Startup Development House sp. z o.o.

Aleje Jerozolimskie 81

Warsaw, 02-001

VAT-ID: PL5213739631

KRS: 0000624654

REGON: 364787848

Contact us

Follow us

logologologologo

Copyright © 2025 Startup Development House sp. z o.o.

EU ProjectsPrivacy policy