Case StudiesBlogO nas
Porozmawiajmy

Wprowadzenie do CQRS w NestJS

Michał Cywiński

03 mar 202317 min czytania

Node.jsBack-end development

Spis treści

  • UserService przed refaktoryzacją

  • Problemy z obecnym UserService

  • CreateUserCommand

  • SendActivationEmailEventHandler

  • GetActiveUsersQuery

  • NestJS controllers

Zdefiniujmy problem

Przez wiele lat większość aplikacji budowano w oparciu o klasyczną architekturę wielowarstwową (N‑warstwową) (zob. poniżej). Jednak nawet dziś, gdy zespoły trzymają się najlepszych praktyk, takich jak stosowanie wzorców projektowych, zasad SOLID i pisanie testów jednostkowych dla serwisów, to podejście bardzo często prowadzi do nadmiernie skomplikowanych baz kodu, trudnych w utrzymaniu. Taką bazę kodu często nazywamy "Big Ball of Mud".

W tym momencie możesz zapytać: „Dlaczego ta Big Ball of Mud powstaje, skoro mój zespół i ja trzymamy się najnowszych praktyk? I dlaczego dzieje się tak w większości projektów, w których biorę udział?” Choć początkowo trzymamy się wspomnianych zasad i wydaje się, że wszystko jest pod kontrolą, z czasem warstwy kodu stają się coraz bardziej złożone. W efekcie wiele serwisów zaczyna odwoływać się do siebie nawzajem, a ich testowanie staje się uciążliwe.

W tym miejscu na scenę wkroczyły mikroserwisy. Wszyscy się nimi zachwycili, bo wyglądało na to, że znaleziono srebrną kulę na powyższe problemy. Po pewnym czasie wiele zespołów znów jednak napotkało te same trudności, tylko jeszcze trudniejsze do rozwiązania: „rozproszoną Big Ball of Mud”. Do tego doszły kwestie komunikacji i synchronizacji danych — nie wspominając o wielu jednostkach wdrożeniowych, którymi trzeba zarządzać. Prawdziwa orkiestra problemów do okiełznania, z którą zespoły mierzą się do dziś.

Jak więc uratować się z tego piekła złożoności? Czy da się mieć piękną bazę kodu, którą aż chce się testować jednostkowo? 

Tak. Pozwól, że przedstawię wzorzec CQRS. Nie rozwiąże wszystkich problemów, ale w zespołach, z którymi pracowałem, CQRS oszczędził nam wielu bóli głowy.

CQRS — definicja

CQRS to akronim od Command Query Responsibility Segregation. Co to jednak w praktyce oznacza? Wzorzec CQRS zakłada użycie trzech głównych typów klas żądań do uruchamiania i orkiestracji logiki biznesowej:

Commands, które odpowiadają za modyfikację stanu danych aplikacji; 

Queries, które służą wyłącznie do odczytu danych aplikacji bez zmiany jej stanu;

Events, które mogą być emitowane w trakcie wykonywania logiki biznesowej — np. aby uruchomić dodatkowe przetwarzanie danych po wykonaniu command.

Każdy z powyższych typów klas żądań ma odpowiadający mu handler, który wykonuje właściwą logikę biznesową. Traktuj handler jak pojedynczą metodę serwisu. Klasy żądań, takie jak polecenia, niosą jedynie dane wejściowe (payload) dla danego handlera.

Ważne jest też to, że commandy i query mają zawsze dokładnie jeden odpowiadający im handler. Eventy mogą mieć wiele handlerów. Przykładowo możemy powiadamiać o zdarzeniu e‑mailem i SMS‑em, co daje nam dwa różne handlery dla każdego typu powiadomienia, reagujące na to samo zdarzenie.

Każdy typ klasy żądania jest publikowany na odpowiedniej szynie: Command Bus, Query Bus lub Event Bus. Te szyny znają zarejestrowane handlery (zwykle przez kontener wstrzykiwania zależności) i wiedzą, który handler należy uruchomić z przekazaną instancją klasy żądania.

Ale co to oznacza w praktyce? Dość teorii — zobaczmy to w działaniu.

CQRS na przykładzie

W poniższym przykładzie poznamy CQRS, refaktoryzując fragment serwisu aplikacyjnego na polecenia, zapytania, zdarzenia oraz ich odpowiadające handlery.

UserService przed refaktoryzacją

Poniżej znajduje się prosta klasa UserService odpowiedzialna za tworzenie użytkownika i pobieranie aktywnych użytkowników.

Logika tworzenia odpowiada za utworzenie użytkownika z zahaszowanym hasłem oraz wysłanie e‑maila aktywacyjnego. Po utworzeniu użytkownika zwracamy ID nowo utworzonego konta.

Logika pobierania odpowiada za odfiltrowanie kont użytkowników tak, by zwrócić jedynie aktywne, a następnie zmapowanie ich do DTO.

Problemy z obecnym UserService

Zanim przejdziemy do refaktoryzacji, warto zastanowić się, jakie problemy mają takie klasy serwisowe i klasyczne podejście do serwisów:

Co ma wspólnego tworzenie użytkownika z pobieraniem aktywnych użytkowników? Poza korzystaniem z tego samego repozytorium do dostępu do danych… właściwie nic.

Jeśli chcemy pisać testy jednostkowe dla pobierania aktywnych użytkowników, musimy zamockować lub wstrzyknąć NotificationService, który nie ma nic wspólnego z getActiveUsers.

W miarę rozwoju projektu serwisy robią się coraz bardziej chaotyczne. Przy takim podejściu ostatecznie lądujemy ze spaghetti code.

Od lat słyszymy, by nie umieszczać logiki biznesowej w kontrolerach. W praktyce przenosimy bałagan z kontrolerów do serwisów. Efekt? Ten sam bałagan ląduje w serwisach zamiast w kontrolerach — i wciąż nie zyskujemy nic w kwestii łatwości utrzymania bazy kodu.

Mając to na uwadze, czas na refaktoryzację. Skorzystamy z doskonałego pakietu dla NestJS, który ma też świetną dokumentację.

CreateUserCommand

Wprowadzenie naszego pierwszego commandu i handlera jest tak proste, jak zadeklarowanie dwóch odpowiadających sobie klas: CreateUserCommand oraz CreateUserCommandHandler (druga musi mieć dekorator @CommandHandler i implementować interfejs ICommandHandler).

W commandzie przechowujemy informacje potrzebne do utworzenia konta użytkownika i modyfikacji stanu aplikacji: adres e‑mail i hasło. Nasz handler zawiera logikę wyekstrahowaną z oryginalnej klasy UserService.

Warto zauważyć, że wstrzykujemy nową zależność: EventBus z @nestjs/cqrs. Służy do wyemitowania zdarzenia informującego, że konto użytkownika zostało utworzone. Handlery zdarzeń (omówione później) mogą niezależnie przechwycić takie zdarzenia i na nie zareagować: w naszym przypadku wysłać e‑mail aktywacyjny z tajnym tokenem.

Co naprawdę cieszy, to fakt, że testy jednostkowe stają się banalnie proste w porównaniu z poprzednim podejściem. Wystarczy sprawdzić, czy użytkownik został zapisany w bazie z poprawnymi właściwościami oraz czy zdarzenie zostało wyemitowane (co można zweryfikować przy pomocy mocka EventBus). 

Nie ma więc znaczenia, ile handlerów zareaguje na opublikowane zdarzenie — każdy z nich testujemy niezależnie. Daje nam to większą swobodę, bo nie polegamy na serwisach takich jak NotificationService, który był wstrzykiwany w początkowym UserService. Dzięki temu logika wyzwalana przy rejestracji użytkownika zostaje odseparowana od samego utworzenia encji użytkownika. Fajne, prawda?

SendActivationEmailEventHandler

Nazwa mówi sama za siebie. To nasz handler zdarzenia, który reaguje na UserCreatedEvent wyemitowane w CreateUserCommandHandler i wysyła e‑mail aktywacyjny na podstawie payloadu przekazanego i zbudowanego w klasie zdarzenia w tym handlerze commanda.

Podobnie jak w przypadku handlerów commandów, musimy udekorować go dekoratorem @EventsHandler i zaimplementować interfejs IEventHandler z @nestjs/cqrs.

To tutaj wstrzykujemy serwis notyfikacji. Zauważ, że możemy teraz testować jednostkowo samo wysyłanie e‑maila aktywacyjnego bez faktycznego tworzenia konta użytkownika. Nasz kod jest znacznie bardziej modułowy.

Co więcej, łatwo wyobrazić sobie kolejny handler zdarzenia reagujący na to samo zdarzenie w sposób niezależny — np. wysyłając SMS. Taki handler również można testować niezależnie od innych handlerów zdarzeń i od handlera commanda, który je wyemitował. Im bardziej zdarzeniowa staje się nasza aplikacja, tym więcej zyskujemy na takiej architekturze.

GetActiveUsersQuery

Na koniec wprowadzimy zrefaktoryzowane GetActiveUsersQuery oraz GetActiveUsersQueryHandler, udekorowane @QueryHandler i implementujące interfejs IQueryHandler.

Klasa query niesie wszystkie niezbędne informacje (payload) do wykonania logiki zdefiniowanej w odpowiadającym handlerze. W naszym przypadku jest to filtr właściwości isActive. Po wykonaniu filtracji wyniki są mapowane do DTO dokładnie tak samo, jak w początkowym UserService.

NestJS controllers

Mając przygotowane wszystkie query i commandy, możemy wstrzyknąć QueryBus i CommandBus do kontrolera API. Handlery zostaną automatycznie dopasowane i wywołane. Pozbywamy się wszelkich zależności (serwisów) poza busami. Czysto, prawda?

Wprowadzanie CQRS do istniejących baz kodu

Ktoś może powiedzieć: „OK, super. Fajnie zobaczyć ten wzorzec, ale pracuję nad starą bazą kodu (legacy). Nie mogę teraz wprowadzić tego do mojego projektu”. To nie jest argument. Możesz wprowadzić ten wzorzec do istniejącej bazy kodu. Czasem łatwo, czasem z dodatkowym wysiłkiem. Przerabiałem to w kodach klasy enterprise — i zawsze było warto.

Nie musisz wdrażać tego wzorca od razu w całości. Właśnie tu CQRS błyszczy: można go wprowadzać stopniowo, bez wywracania projektu do góry nogami. Nowe funkcjonalności w większości da się od razu implementować jako handlery commandów i query.  Tak, natychmiastowa korzyść. 

Oczywiście czasem będzie to wymagało wstrzyknięcia skomplikowanych serwisów (czego w idealnym scenariuszu wolelibyśmy unikać), ale wciąż możesz je mockować, by ułatwić sobie testy jednostkowe — i życie.

W przypadku istniejących serwisów możesz zacząć refaktoryzację od wyodrębniania handlerów, gdy tylko ruszysz metodę serwisu związaną z Twoim aktualnym tikietem w Jira/Trello/twoim ulubionym narzędziu do śledzenia zadań. Zacznij po prostu od przeniesienia logiki do nowego handlera bez modyfikacji, aby nie mieszać nowej funkcjonalności ze sprzątaniem. Jak zawsze pamiętaj o dodaniu testów jednostkowych, jeśli wcześniej ich nie było. 

Utwórz pull request, upewnij się, że wszyscy rozumieją zrefaktoryzowany kod, i zmerguj go do gałęzi deweloperskiej. Teraz czas na właściwe zadanie. Powinno być znacznie łatwiej dodać nową funkcję, bo operujesz wyłącznie na poziomie handlerów CQRS. 

Tak, do tego właśnie dążyliśmy: ultraproste klasy, wyspecjalizowane w robieniu dokładnie jednej rzeczy i używające tylko tych zależności, których naprawdę potrzebują.

Final thoughts

CQRS może wymagać dopisania nieco dodatkowego kodu niosącego payload dla naszych handlerów, ale w zamian niezwykle ułatwia tworzenie wielu małych, łatwych w utrzymaniu klas, które świetnie się testuje jednostkowo.

Czy poleciłbym go do każdego projektu? Prawdopodobnie nie. Jeśli potrzebujesz jedynie kilku prostych CRUD‑ów i zależy Ci na szybkim dowożeniu, być może lepiej trzymać się starych, dobrych klas serwisów.  Warto jednak rozważyć refaktoryzację starego kodu, gdy tylko zmieni się zakres projektu albo wzrośnie jego złożoność.

W moim developerskim niezbędniku dla projektów średnich i dużych CQRS to zawsze właściwa droga. To się zwróci — obiecuję.

 

Opublikowany 03 marca 2023

Udostępnij


Michał Cywiński

Fullstack developer

Digital Transformation Strategy for Siemens Finance

Cloud-based platform for Siemens Financial Services in Poland

See full Case Study
Ad image
White-label smart access app connected to smart locks
Nie przegap żadnego artykułu - zapisz się do naszego newslettera
Zgadzam się na otrzymywanie komunikacji marketingowej od Startup House. Kliknij, aby zobaczyć szczegóły

Może Ci się również spodobać...

Jak wybrać najlepszą agencję Node.js w 2023 roku: kompleksowy przewodnik
Node.jsDigital products

Jak wybrać najlepszą agencję Node.js w 2023 roku: kompleksowy przewodnik

Node.js stał się kluczową technologią do tworzenia niezawodnych, skalowalnych aplikacji webowych. W tym kontekście rola firm programistycznych specjalizujących się w Node.js jest ważniejsza niż kiedykolwiek. Ten artykuł pomoże Ci wybrać najlepsze firmy specjalizujące się w Node.js, dopasowane do Twoich potrzeb.

Marek Pałys

02 lut 20236 min czytania

Ruby on Rails - guide
Ruby on RailsBack-end developmentComputer programming

Jak zainstalować Ruby i Ruby on Rails oraz używać RubyGems

Poznaj przewodnik krok po kroku, który pokaże, jak zainstalować i wykorzystać Ruby on Rails do efektywnego tworzenia aplikacji internetowych. Dowiesz się, jak zainstalować i skonfigurować Ruby, zarządzać różnymi wersjami, korzystać z RubyGems i Bundlera oraz stworzyć nowy projekt w Ruby on Rails. Rozpocznij swoją przygodę z tworzeniem aplikacji w Ruby on Rails dzięki temu kompleksowemu przewodnikowi.

Jan Grela

20 mar 20206 min czytania

Stack technologiczny 2020: GraphQL, Apollo Server i React.js
Back-end developmentProduct developmentGraphQL

Stack technologiczny 2020: GraphQL, Apollo Server i React.js

Od 2000 roku zasady RESTful są branżowym standardem w tworzeniu web API, jednak GraphQL oferuje bardziej efektywne rozwiązanie. W tym artykule omawiamy zalety GraphQL w porównaniu z REST, jego popularność oraz to, jak za pomocą Apollo GraphQL zbudować serwer i aplikację kliencką.

Wojciech Cichoradzki

12 maj 20207 min czytania

Gotowy, aby scentralizować swoje know-how z pomocą AI?

Rozpocznij nowy rozdział w zarządzaniu wiedzą — gdzie Asystent AI staje się centralnym filarem Twojego cyfrowego wsparcia.

Umów bezpłatną konsultację

Pracuj z zespołem, któremu ufają firmy z czołówki rynku.

Rainbow logo
Siemens logo
Toyota logo

Budujemy to, co będzie dalej.

Firma

Startup Development House sp. z o.o.

Aleje Jerozolimskie 81

Warszawa, 02-001

VAT-ID: PL5213739631

KRS: 0000624654

REGON: 364787848

Kontakt

hello@startup-house.com

Nasze biuro: +48 789 011 336

Nowy biznes: +48 798 874 852

Obserwuj nas

Award
logologologologo

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

UE ProjektyPolityka prywatności