Programming/기타

A Command Bus Solution for CQRS

armyost 2025. 4. 16. 23:16
728x90
import { CommandBus, QueryBus } from '@nestjs/cqrs';

@Controller()
export class EmployeeController {
  constructor(
    private readonly commandBus: CommandBus,
    private readonly queryBus: QueryBus,
  ) {}

 

 

Commands

Commands are used to change the application state. They should be task-based, rather than data centric. When a command is dispatched, it is handled by a corresponding Command Handler. The handler is responsible for updating the application state.

JS
@Injectable()
export class HeroesGameService {
  constructor(private commandBus: CommandBus) {}

  async killDragon(heroId: string, killDragonDto: KillDragonDto) {
    return this.commandBus.execute(
      new KillDragonCommand(heroId, killDragonDto.dragonId)
    );
  }
}

In the code snippet above, we instantiate the KillDragonCommand class and pass it to the CommandBus's execute() method. This is the demonstrated command class:

JS
export class KillDragonCommand extends Command<{
  actionId: string // This type represents the command execution result
}> {
  constructor(
    public readonly heroId: string,
    public readonly dragonId: string,
  ) {}
}

As you can see, the KillDragonCommand class extends the Command class. The Command class is a simple utility class exported from the @nestjs/cqrs package that lets you define the command's return type. In this case, the return type is an object with an actionId property. Now, whenever the KillDragonCommand command is dispatched, the CommandBus#execute() method return-type will be inferred as Promise<{ actionId: string }>. This is useful when you want to return some data from the command handler.

 

The CommandBus represents a stream of commands. It is responsible for dispatching commands to the appropriate handlers. The execute() method returns a promise, which resolves to the value returned by the handler.

Let's create a handler for the KillDragonCommand command.

JS
@CommandHandler(KillDragonCommand)
export class KillDragonHandler implements ICommandHandler<KillDragonCommand> {
  constructor(private repository: HeroesRepository) {}

  async execute(command: KillDragonCommand) {
    const { heroId, dragonId } = command;
    const hero = this.repository.findOneById(+heroId);

    hero.killEnemy(dragonId);
    await this.repository.persist(hero);

    // "ICommandHandler<KillDragonCommand>" forces you to return a value that matches the command's return type
    return {
      actionId: crypto.randomUUID(), // This value will be returned to the caller
    }
  }
}

This handler retrieves the Hero entity from the repository, calls the killEnemy() method, and then persists the changes. The KillDragonHandler class implements the ICommandHandler interface, which requires the implementation of the execute() method. The execute() method receives the command object as an argument.

Note that ICommandHandler<KillDragonCommand> forces you to return a value that matches the command's return type. In this case, the return type is an object with an actionId property. This only applies to commands that inherit from the Command class. Otherwise, you can return whatever you want.

Lastly, make sure to register the KillDragonHandler as a provider in a module:

providers: [KillDragonHandler];

 

 

Queries

Queries are used to retrieve data from the application state. They should be data centric, rather than task-based. When a query is dispatched, it is handled by a corresponding Query Handler. The handler is responsible for retrieving the data.

The QueryBus follows the same pattern as the CommandBus. Query handlers should implement the IQueryHandler interface and be annotated with the @QueryHandler() decorator. See the following example:

content_copy

export class GetHeroQuery extends Query<Hero> {
  constructor(public readonly heroId: string) {}
}

Similar to the Command class, the Query class is a simple utility class exported from the @nestjs/cqrs package that lets you define the query's return type. In this case, the return type is a Hero object. Now, whenever the GetHeroQuery query is dispatched, the QueryBus#execute() method return-type will be inferred as Promise<Hero>.

To retrieve the hero, we need to create a query handler:

JS
@QueryHandler(GetHeroQuery)
export class GetHeroHandler implements IQueryHandler<GetHeroQuery> {
  constructor(private repository: HeroesRepository) {}

  async execute(query: GetHeroQuery) {
    return this.repository.findOneById(query.hero);
  }
}

The GetHeroHandler class implements the IQueryHandler interface, which requires the implementation of the execute() method. The execute() method receives the query object as an argument, and must return the data that matches the query's return type (in this case, a Hero object).

Lastly, make sure to register the GetHeroHandler as a provider in a module:

providers: [GetHeroHandler];

 

Now, to dispatch the query, use the QueryBus:

const hero = await this.queryBus.execute(new GetHeroQuery(heroId)); // "hero" will be auto-inferred as "Hero" type