top of page

CQRS vs Classical n-layer application


When building an API the typical practice is to divide it into parts, simplest would be presentation, service / business logic and data access layers



3-layer architecture


There is nothing wrong with this approach but questions araise when application becomes bloated and unreadable.I prepared simple user’s microservice with basic functionality to make case-study on it. But first things first.


CQS

Must be mentioned before. It stands for Command-query Separation. The simplest explanation would be:



CQS


It separates two main sections of our application. Queries are thoose actions which doesn’t change data (in most cases) and return something whereas commands are those which change data and return nothing (again, in most cases). Most likely that you used it even without knowing it because it’s kinda natural approach to the problem. It allows us to:

  1. Distributed responsibility. When working in team, less experienced developers may receive queries to take care about because they don’t mess up with data.

  2. Clean code. However, this argument is often overused. I’ll explained it later.


CQRS

Command Query Responsibility Segregation, idea presented by Greg Young and Udi Dahan. Let’s stop for a moment. In most modern frameworks we use OOP right? Yeah but where thoose objects can be seen in above diagrams? I’m not talking about singleton pattern, which creates single instance of eg. service class in the app because it serves another purpose and would be cheap answer.


And that’s the problem. Actions in classic CQS are handled by methods in the end. CQRS is handling them by objects, an instance of action class with one or more methods.



CQRS


Prerequisites

  1. Nest.js

  2. Sequelize ORM

  3. Swagger.io (api docs)


Practise

API looks as follows:



Simple REST CRUD microservice. Additionally user can login using his name or email and view history. Application has two modules with same set of routes. The ‘simple’ variant is made with classical approach and the other one with CQRS.


Let’s look into the code:

import {Body,Controller,Delete,Get,Post,Put,Req,UseGuards} from '@nestjs/common';
import {ApiBody, ApiCreatedResponse, ApiOkResponse, ApiParam, ApiTags} from' @nestjs/swagger';
import {Request} from 'express';
import {UserSimpleService} from './user-simple.service';
import {GetUserDto} from './dto/get.dto';
import {ModifyUserDto} from './dto/modify.dto';
import {UserLoginHistoryDto} from './dto/login-history.dto';
import {AccessUserSimple} from './guards/access.guard';
import {IRequestUserSimple} from './interfaces/request.interface';
import {LoginInputDto} from './dto/login-input.dto';

@ApiTags('User simple')
@UseGuards(AccessUserSimple)
@Controller('user-simple')
export class UserSimpleController
{
  public constructor(private userSimpleService: UserSimpleService){}  
    
    @Get() 
    @ApiOkResponse({type: [GetUserDto]})
    public List()
    {
        return this.userSimpleService.List();
    }  
    
    @Get(':id')  
    @ApiParam({name: 'id'})  
    @ApiOkResponse({type: GetUserDto})
    public GetUser (@Req()req: IRequestUserSimple)
    {
        return this.userSimpleService.GetUser(req.user);
    }  
    
    @Get(':id/login-history')  
    @ApiParam({name: 'id'})  
    @ApiOkResponse({type: [UserLoginHistoryDto]})
    public GetLoginHistory(@Req()req: IRequestUserSimple)
    {
        return this.userSimpleService.GetLoginHistory(req.user.id);
    }  
    
    @Post('login')  
    @ApiBody({type: LoginInputDto})  
    @ApiCreatedResponse({type: GetUserDto})
    public Login(@Req()req: Request, @Body()data: LoginInputDto)
    {
        return this.userSimpleService.Login(req,data);
    }  
    
    @Post() 
    @ApiBody({type: ModifyUserDto})  
    @ApiCreatedResponse({type: GetUserDto})
    public CreateUser(@Body()data: ModifyUserDto)
    {
        return this.userSimpleService.CreateUser(data);
    }  
    
    @Put(':id')  
    @ApiParam({name: 'id'})  
    @ApiBody({type: ModifyUserDto})  
    @ApiOkResponse({type: GetUserDto})
    public UpdateUser(@Req()req: IRequestUserSimple, @Body()data: ModifyUserDto)
    {
        return this.userSimpleService.UpdateUser(req.user.id,data);
    }  
    
    @Delete(':id')  
    @ApiParam({name: 'id'})
    public RemoveUser(@Req()req: IRequestUserSimple)
    {
        return this.userSimpleService.RemoveUser(req.user.id);
    }
}


Controller uses service with those methods:

import {BadRequestException,Injectable,UnauthorizedException} from '@nestjs/common';
import {ConfigService} from '@nestjs/config';
import {InjectModel} from '@nestjs/sequelize';
import {Sequelize,Op,ValidationErrorItem} from 'sequelize';
import {sign} from 'jsonwebtoken';
import {Request} from 'express';
import {UserLoginHistoryDto} from './dto/login-history.dto';
import {ModifyUserDto} from './dto/modify.dto';
import {GetUserDto} from './dto/get.dto';
import {LoginInputDto} from './dto/login-input.dto';
import {LoginUserDto} from './dto/login-user.dto';
import {LoginDto} from './dto/login.dto';
import {User} from 'src/entities/user.entity';
import {LoginHistory} from 'src/entities/user-login-history.model';

@Injectable()
export class UserSimpleService
{
    public constructor(
        private config: ConfigService,
        private sequelize: Sequelize,    
        @InjectModel(User)private userModel: typeof User,    
        @InjectModel(LoginHistory) private loginHistoryModel: 
                                            typeofcLoginHistory)
        {}
    
    public async GetUser(user: User): Promise<GetUserDto>{
        return{
            id: user.id,
            name: user.name,
            fullName: user.fullName,
            description: user.description,
            email: user.email,
            lang: user.lang,
            active: user.active,
            emailNotification: user.emailNotification,
            createdAt: user.createdAt,
            updatedAt: user.updatedAt
        };
    }
    
    public async List(): Promise<GetUserDto[]>
    {
        const users = await this.userModel.findAll({raw: true});
        return <GetUserDto[]>users;
    }
    
    public async GetLoginHistory(userId: number): Promise<UserLoginHistoryDto[]>
    {
        const history = await this.loginHistoryModel.findAll({where: 
                                                    { userId }});
        return history.map(entry=>({address: entry.address,browser: 
                                                entry.browser}));
    }
    
    public async Login(req: Request,data: LoginInputDto): 
                                            Promise<LoginDto>{
        const user = await this.userModel.findOne(
        {
            attributes: 
            [
                'id',
                'name',
                'fullName',
                'email',
                'password',
                'lang'
            ],
            where: 
            {
            [
                Op.or]: 
                [
                    {name: data.nameOrEmail},
                    {email: data.nameOrEmail}
                ],
                password: {[Op.ne]: null},
                active: true}});
        
        if (!user||!user.ComparePassword(data.password))
        {
            throw new UnauthorizedException();
        }
        
        await this.loginHistoryModel.create({
            address: req.headers['x-forwarded-
                            for']||req.connection.remoteAddress,
            browser: req.headers['user-agent'],
            userId: user.id});
        
        const userData: LoginUserDto={
            name: user.name,
            fullName: user.fullName,
            email: user.email,
            lang: user.lang};
            
        const jwt=sign(userData,this.config.get<string>
                                ('authentication.jwtSecret'),{
        expiresIn: this.config.get<number>
                                ('authentication.expiresIn')
        });
        
        return { ...userData, jwt };
     }
    
     public async CreateUser(data: ModifyUserDto): Promise<GetUserDto>
     {
         let user: User;
         try
         {
             user = await this.userModel.create(data,{raw: true});
         }
         catch (err)
         {
             this.HandleDatabaseError(err);
         }
         return <GetUserDto>user;
     }
     
     public async UpdateUser(userId: number,data: ModifyUserDto)
     {
         return this.sequelize.transaction(async t =>
         {
             try
             {
                 await this.userModel.update(data,
                 {
                     where: 
                     {
                         id: userId},
                         transaction: t
                     }
                 );
             }
             catch(err)
             {
                 this.HandleDatabaseError(err);
             }
             return this.userModel.findByPk(userId,
             {
                 transaction: t,
                 raw: true
             });
         });
     }
     
     public async RemoveUser(userId: number)
     {
         await this.userModel.destroy({where: {id: userId}});
     }
     
     private HandleDatabaseError(err: any): void
     {
         const errors: ValidationErrorItem[] = err.errors;
         if (!(errors&&errors[0]instanceof ValidationErrorItem))
         {
             throw err;
         }
         throw new BadRequestException(errors[0].message);
     }
 }

It takes only 116 lines of code so not a big deal but will arise when scaling because of no clear way to organize code.


Implementing CQRS

Let’s analyze project structure.



Here we have two controllers:

import {Body,Controller,Delete,Post,Put,Req,UseGuards} from '@nestjs/common';
import {CommandBus} from '@nestjs/cqrs';
import {ApiBody,ApiCreatedResponse,ApiOkResponse,ApiParam,ApiTags} from '@nestjs/swagger';
import {GetUserDto} from 'src/user-simple/dto/get.dto';
import {ModifyUserDto} from 'src/user-simple/dto/modify.dto';
import {CreateUserCommand} from './commands/create-user/create-user.command';
import {RemoveUserCommand} from './commands/remove-user/remove-user.command';
import {UpdateUserCommand} from './commands/update-user/update-user.command';
import {IRequestUser} from './interfaces/request.interface';
import {AccessUser} from './guards/access.guard';

@ApiTags('Users')
@UseGuards(AccessUser)
@Controller('users')
export class UserCommandController
{
    public constructor(private readonly commandBus: CommandBus){}  
    
    @Post()  
    @ApiBody({type: ModifyUserDto})  
    @ApiCreatedResponse({type: GetUserDto})
    public CreateUser(@Body()data: ModifyUserDto): 
                                            Promise<GetUserDto>
    {
        return this.commandBus.execute(new CreateUserCommand(data));
    }  
    
    @Put(':id')  
    @ApiParam({name: 'id'})  
    @ApiBody({type: ModifyUserDto})  
    @ApiOkResponse({type: GetUserDto})
    public UpdateUser(@Req()req: IRequestUser, @Body()data: 
                            ModifyUserDto): Promise<GetUserDto>
    {
        return this.commandBus.execute(new UpdateUserCommand
                                    (req.user.data.id,data));
    }  
    
    @Delete(':id')  
    @ApiParam({name: 'id'})
    publicRemoveUser(@Req()req: IRequestUser)
    {
        return this.commandBus.execute(new  RemoveUserCommand( 
                                            req.user.data.id));
    }
}


import {Body,Controller,Get,Post,Req,UseGuards} from '@nestjs/common';
import {ApiBody,ApiCreatedResponse,ApiOkResponse,ApiParam,ApiTags} from '@nestjs/swagger';
import {QueryBus} from '@nestjs/cqrs';
import {Request} from 'express';
import {IRequestUser} from './interfaces/request.interface';
import {AccessUser} from './guards/access.guard';
import {UserLoginHistoryDto} from './dto/login-history.dto';
import {GetUserDto} from './dto/get.dto';
import {ListUsersQuery} from './queries/list-users/list-users.query';
import {GetUserQuery} from './queries/get-user/get-user.query';
import {GetLoginHistoryQuery} from './queries/login-history/login-history.query';
import {LoginInputDto} from './dto/login-input.dto';
import {LoginQuery} from './queries/login/login.query';
import {LoginDto} from './dto/login.dto';

@ApiTags('Users')
@UseGuards(AccessUser)
@Controller('users')
export class UserQueryController
{
    public constructor(private readonly queryBus: QueryBus){} 
    
    @Get()  
    @ApiOkResponse({type: [GetUserDto]})
    public List()
    {
        return this.queryBus.execute<ListUsersQuery, GetUserDto[]>
                                            (new ListUsersQuery());
    }  
    
    @Get(':id')  
    @ApiParam({name: 'id'})  
    @ApiOkResponse({type: GetUserDto})
    public GetUser(@Req()req: IRequestUser)
    {
        return this.queryBus.execute<GetUserQuery, GetUserDto>
                                       (new GetUserQuery(req.user));
    }  
    
    @Get(':id/login-history')  
    @ApiParam({name: 'id'})  
    @ApiOkResponse({type: [UserLoginHistoryDto]})
    public GetLoginHistory(@Req()req: IRequestUser)
    {
        return this.queryBus.execute<GetLoginHistoryQuery, 
                UserLoginHistoryDto[]>(new GetLoginHistoryQuery
                (req.user.data.id));
    }  
    
    @Post('login')  
    @ApiBody({type: LoginInputDto})  
    @ApiCreatedResponse({type: GetUserDto})
    public Login(@Req()req: Request, @Body()data: LoginInputDto)
    {
        return this.queryBus.execute<LoginQuery, LoginDto>
                                        (new LoginQuery(req,data));
    }
}


As you may have noticed queries and commands in form of object. Second important aspect is that we have no service in this module. It’s functionality is distributed in queries and commands directories.


Each action has its handler. Let’s look into one:

import {IQueryHandler,QueryHandler} from '@nestjs/cqrs';
import {InjectModel} from '@nestjs/sequelize';
import {User} from 'src/entities/user.entity';
import {ListUsersQuery} from './list-users.query';
import {GetUserDto} from 'src/user/dto/get.dto';

@QueryHandler(ListUsersQuery)
export class ListUsersHandler implements 
                                    IQueryHandler<ListUsersQuery>
{
    public constructor (@InjectModel(User) private userModel: typeof 
                                                            User){}
    
    public async execute(): Promise<GetUserDto[]>
    {
        const users = await this.userModel.findAll({raw: true});
        return <GetUserDto[]>users;
    }
}

It has same functionality as List() method in service.


Another example:

import {UnauthorizedException} from '@nestjs/common';
import {IQueryHandler,QueryHandler} from '@nestjs/cqrs';
import {ConfigService} from '@nestjs/config';
import {InjectModel} from '@nestjs/sequelize';
import {Op} from 'sequelize';
import {Request} from 'express';
import {sign} from 'jsonwebtoken';
import {User} from 'src/entities/user.entity';
import {LoginQuery} from './login.query';
import {LoginUserDto} from 'src/user/dto/login-user.dto';
import {LoginDto} from 'src/user/dto/login.dto';
import {LoginHistory} from 'src/entities/user-login-history.model';

@QueryHandler(LoginQuery)
export class LoginHandler implements IQueryHandler<LoginQuery>
{
    public constructor(
        private config: ConfigService,    
        @InjectModel(LoginHistory) private loginHistoryEntity: 
                                             typeof LoginHistory,    
        @InjectModel(User) private userModel: typeof User)
        {}
    
    public async execute(query: LoginQuery): Promise<LoginDto>
    {
        const userEntity = await this.userModel.findOne({
            attributes: 
            [
                'id',
                'name',
                'fullName',
                'email',
                'password',
                'lang'
            ],
            where: {
                [Op.or]: [{name: query.data.nameOrEmail},
                                {email: query.data.nameOrEmail}],
                password: {[Op.ne]: null},
                active: true
             }
         });
         if (!userEntity||!userEntity.ComparePassword
                                (query.data.password))
         {
            throw new UnauthorizedException();
         }
         
         await this.AddToHistory(userEntity.id,query.req);
         const  userData: LoginUserDto={
             name: userEntity.name,
             fullName: userEntity.fullName,
             email: userEntity.email,
             lang: userEntity.lang
         };
         
         return{ ...userData,jwt: this.CreateJwt(userData)};
     }
     
     private CreateJwt(data: LoginUserDto): string{
         return sign(data,this.config.get<string>
             ('authentication.jwtSecret'),
             {expiresIn: this.config.get<number>
             ('authentication.expiresIn')
         });
     }
     
     private async AddToHistory(userId: number,req: Request)
     {
         return this.loginHistoryEntity.create({
             address: req.headers['x-forwarded-
                             for']||req.connection.remoteAddress,
             browser: req.headers['user-agent'],      
             userId
         });
     }
 }

As you see here we have more breathing space. Method does no longer needs to be long or cut into many messing with code organization. Simillary to main() method known from C, here execute() is beeing fired when handling query.


Note that we can’t always separate read and write blocks. Login query creates history entry violating pure Separation rule. But implementing two separate routes would be an art for art.


Domain-Driven Design elements

Project contains new class called User.

import { AggregateRoot } from '@nestjs/cqrs';
import { User } from 'src/entities/user.entity';

export class UserAggregate extends AggregateRoot{
    public constructor(public readonly data: User){
        super();
    }
    
    public FormatData(){
        return{
            id: this.data.id,
            name: this.data.name,
            fullName: this.data.fullName,
            description: this.data.description,
            email: this.data.email,
            lang: this.data.lang,
            active: this.data.active,
            emailNotification: this.data.emailNotification,
            createdAt: this.data.createdAt,
            updatedAt: this.data.updatedAt
        };
    }
}

It’s object in our project domain and aggregate root. It means that we can talk about it and manipulate on it independently. Good example is basket in online shop app. There is no sense in implementing functionality for order positions because they don’t exists without basket. We don’t create data access to as well. In simpliest implementation it could be entity class from sequalize but this practise have a lot of flaws.


In this API it could be easily omitted and Domain-Driven Design is topic for another article. I mencioned it because it is present in official Nest.js CQRS documentation as well as Event Sourcing which is not part of CQRS. I’ll be demystifying thoose terms in next article.


Summary

Is CQRS worth it, at least considering example discussed in article? It depends on horizon of the project. If it’s going to remain simple and there are 1–2 programmers responsible for it there is no need to complicate matter even more.


But things change when code begins to grow without control. There is no need to decide on one approach in scope of whole project. Simple modules may still be based on simple controller-service setup whereas more complicated, especially contaning long in implementation routes should be considered as worthy CQRS candidates.



Source: Medium - Mateusz Fonfara


The Tech Platform

0 comments

Recent Posts

See All

Комментарии


bottom of page