명령, 처리기 및 CommandBus
쓰기 작업을 명령으로 모델링하고 CommandBus를 통해 전용 처리기로 전달합니다.
명령, 처리기 및 CommandBus은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Commands Exist
In a CQRS system we split the model into two halves: the write side that changes state and the read side that returns data. A Command represents an intent to change state, for example CreateOrder or CancelSubscription.
- A command is a plain DTO carrying the data needed to perform the write.
- It is named in the imperative (do this) and describes a single business action.
- It returns at most an identifier or acknowledgment, never a full read model.
NestJS ships @nestjs/cqrs, which gives us a CommandBus to dispatch commands to exactly one handler.
Modeling a Command
A command is just a class. It holds the input data as readonly properties so it stays immutable once created. There is no logic inside it.
Below, CreateOrderCommand captures everything a handler needs to create an order. Notice it carries no NestJS decorators and no behavior.
export class CreateOrderCommand {
constructor(
public readonly customerId: string,
public readonly items: { sku: string; quantity: number }[],
public readonly currency: string,
) {}
}The Command Handler Contract
Each command is processed by exactly one handler. In NestJS you implement ICommandHandler<TCommand> and decorate the class with @CommandHandler(TCommand).
@CommandHandlerregisters the link between a command type and its handler.- The interface forces an
execute(command)method. - The handler is a normal provider, so it can inject repositories and services.
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateOrderCommand } from './create-order.command';
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler
implements ICommandHandler<CreateOrderCommand>
{
async execute(command: CreateOrderCommand): Promise<{ orderId: string }> {
const orderId = crypto.randomUUID();
// persist order, charge, etc.
return { orderId };
}
}Dispatching Through the CommandBus
Controllers and other entry points do not call handlers directly. They build a command and hand it to the CommandBus via execute(). The bus finds the registered handler and runs it.
This indirection means the controller stays thin and knows nothing about persistence, validation rules, or side effects.
import { Body, Controller, Post } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { CreateOrderCommand } from './create-order.command';
@Controller('orders')
export class OrdersController {
constructor(private readonly commandBus: CommandBus) {}
@Post()
async create(@Body() dto: CreateOrderDto) {
return this.commandBus.execute(
new CreateOrderCommand(dto.customerId, dto.items, dto.currency),
);
}
}Wiring It Up with CqrsModule
For the bus to discover handlers, you must import CqrsModule in the feature module and register every handler as a provider. NestJS scans providers for the @CommandHandler metadata at startup and binds them to the bus.
- Forget to list the handler in
providersand the bus throws an unhandled command error at dispatch time. CqrsModulesuppliesCommandBus,QueryBus, andEventBus.
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { OrdersController } from './orders.controller';
import { CreateOrderHandler } from './create-order.handler';
@Module({
imports: [CqrsModule],
controllers: [OrdersController],
providers: [CreateOrderHandler],
})
export class OrdersModule {}One Command, One Handler
The CommandBus enforces a strict one-to-one mapping: a single command type resolves to a single handler. This is the core distinction from events.
- Commands express an instruction that may be rejected and have exactly one handler.
- Events announce that something already happened and may have zero or many handlers.
If you register two handlers for the same command, the last one registered wins and the bus silently overrides the earlier binding, which is almost always a bug.
Returning Results from a Command
Strict CQRS purists return nothing from a command. In practice, returning a small acknowledgment such as the new aggregate id is pragmatic and common in NestJS APIs.
CommandBus.execute<TCommand, TResult>() is generic, so you can type the return value precisely. Keep the payload tiny: an id, a status, never a full read model. If the client needs the whole resource, it issues a separate query afterward.
const result = await this.commandBus.execute<
CreateOrderCommand,
{ orderId: string }
>(new CreateOrderCommand(customerId, items, currency));
return { id: result.orderId };Validation Belongs at the Edge
Keep handlers focused on business behavior. Structural validation (required fields, types, formats) belongs on the incoming DTO using class-validator and the global ValidationPipe, before a command is ever constructed.
Business invariants that need data from the database, such as 'customer must not exceed credit limit', belong inside the handler where you have access to repositories.
import { IsArray, IsString, Length } from 'class-validator';
export class CreateOrderDto {
@IsString()
customerId: string;
@IsArray()
items: { sku: string; quantity: number }[];
@IsString()
@Length(3, 3)
currency: string;
}Composing Side Effects in a Handler
A handler typically orchestrates several steps: load state, mutate it, persist, then publish domain events. Injected providers make this clean and testable.
Here the handler persists the order and then publishes an event so other parts of the system can react asynchronously, keeping the write path decoupled from downstream concerns like email or analytics.
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler
implements ICommandHandler<CreateOrderCommand>
{
constructor(
private readonly orders: OrderRepository,
private readonly eventBus: EventBus,
) {}
async execute(command: CreateOrderCommand) {
const order = Order.create(command.customerId, command.items);
await this.orders.save(order);
this.eventBus.publish(new OrderCreatedEvent(order.id));
return { orderId: order.id };
}
}Testing the Pure Logic
Because a command is a plain immutable object, the dispatch-and-handle pattern is easy to reason about. The snippet below is framework-free: it models a tiny command bus, registers a handler, and dispatches a command, demonstrating the exact one-command-one-handler contract you saw earlier.
type Handler<C, R> = (command: C) => R;
class MiniCommandBus {
private handlers = new Map<string, Handler<any, any>>();
register<C, R>(name: string, handler: Handler<C, R>): void {
this.handlers.set(name, handler);
}
execute<R>(name: string, command: unknown): R {
const handler = this.handlers.get(name);
if (!handler) throw new Error(`No handler for ${name}`);
return handler(command);
}
}
class CreateOrderCommand {
constructor(public readonly customerId: string) {}
}
const bus = new MiniCommandBus();
bus.register('CreateOrder', (c: CreateOrderCommand) => ({
orderId: 'ord_' + c.customerId,
}));
const result = bus.execute<{ orderId: string }>(
'CreateOrder',
new CreateOrderCommand('42'),
);
console.log(result.orderId);Error Handling and Idempotency
Commands can fail, and callers need a clear contract. Throw domain exceptions from the handler and let a NestJS exception filter map them to HTTP status codes.
- Throwing inside
execute()rejects the promise returned bycommandBus.execute(). - For at-least-once delivery (retries, message queues), make commands idempotent: include a client-supplied key so re-processing the same command is safe.
Never swallow errors silently in a handler; an undelivered write looks like success to the client.
Quick Check
Test your understanding of the command dispatch model.
Recap
You now know how to model write operations as commands in NestJS CQRS:
- A command is an immutable DTO expressing an intent to change state, named imperatively.
- A handler implements
ICommandHandlerand is bound via@CommandHandler; it holds the business logic and injected dependencies. - The CommandBus dispatches each command to its single handler; controllers stay thin and never call handlers directly.
- Import
CqrsModuleand register handlers inprovidersso the bus can discover them. - Structural validation lives on the DTO at the edge; business invariants live in the handler. Return only a tiny acknowledgment, and make commands idempotent when delivery may retry.
자주 묻는 질문
“명령, 처리기 및 CommandBus” 강의는 무료인가요?
네 — “명령, 처리기 및 CommandBus” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
“명령, 처리기 및 CommandBus”에서 뭘 배우나요?
쓰기 작업을 명령으로 모델링하고 CommandBus를 통해 전용 처리기로 전달합니다. 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“명령, 처리기 및 CommandBus” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 명령, 처리기 및 CommandBus
- 쿼리와 읽기 모델 프로젝션
- 도메인 이벤트와 AggregateRoot
- 장시간 실행되는 워크플로를 위한 사가