NestJS & @cqrs-ddd: Integrating or Migrating
Teams using NestJS have two architectural choices when adopting @cqrs-ddd:
- Keep NestJS and enhance it with
@cqrs-ddd/nestjs: Keep your existing@nestjs/cqrshandlers, modules, dependency injection, and controllers. The adapter wraps handlers in behavior pipelines, provides global exception filtering, and manages correlation and job context. - Replace NestJS completely with
@cqrs-ddd/cqrs: Build framework-free applications on plain Node.js, Fastify, or Express, constructing handlers withnewand registering them oncreateCqrs().
Architectural Comparison
Section titled “Architectural Comparison”| Dimension | Raw @nestjs/cqrs |
NestJS + @cqrs-ddd/nestjs |
Framework-Free @cqrs-ddd/cqrs |
|---|---|---|---|
| Handler Decorators | @CommandHandler, @QueryHandler, @EventsHandler |
Same @nestjs/cqrs decorators + @UsePipeline, @SkipPipeline |
Same names, from @cqrs-ddd/cqrs + @UsePipeline, @SkipPipeline |
| Buses | CommandBus, QueryBus, EventBus |
Same @nestjs/cqrs buses |
Same bus names, from createCqrs() |
| Pipeline Behaviors | None (requires Nest interceptors) | Compiles and executes @cqrs-ddd/pipeline around handlers |
Compiles and executes @cqrs-ddd/pipeline around handlers |
| DI & Containers | NestJS IoC container | NestJS IoC container injects behaviors and handlers | No container: explicit new in composition root |
| Exception Handling | Manual exception filters | Automatic ErrorFilter mapping package errors to HttpException |
Framework-neutral: HTTP layer maps errors (e.g. domainErrorHttpStatus) |
| Startup Diagnostics | Silent overrides on duplicates | Validates singleton scope, provider uniqueness, behavior contracts | Fails on duplicate handlers, missing decorators, contract violations |
| Runtime | NestJS | NestJS | No framework: the buses and the pipelines only |
Option 1: Integrating with NestJS via @cqrs-ddd/nestjs
Section titled “Option 1: Integrating with NestJS via @cqrs-ddd/nestjs”If you are already running on NestJS or want Nest’s dependency injection and controller ecosystem, install @cqrs-ddd/nestjs.
pnpm add @cqrs-ddd/nestjs @cqrs-ddd/pipeline @cqrs-ddd/core @nestjs/cqrsWhat You Keep
Section titled “What You Keep”- All
@Module(),@Injectable(),@Controller()definitions. - Official
@nestjs/cqrsimports:CqrsModule,CommandBus,QueryBus,EventBus,@CommandHandler,@QueryHandler,@EventsHandler. - Handlers implement
ICommandHandler,IQueryHandler,IEventHandler. - Controllers inject
CommandBusandQueryBusas usual.
What You Add
Section titled “What You Add”-
PipelineModule.forRoot()inAppModule:@Module({imports: [CqrsModule.forRoot(),PipelineModule.forRoot({sources: { tenantId: tenantSource, correlationId: correlationSource },globalBehaviors: [{ scope: 'all', before: [logging()] }],}),],providers: [{ provide: APP_FILTER, useClass: ErrorFilter },{ provide: LoggingBehavior, useFactory: () => new LoggingBehavior(new Logger('Pipeline')) },],})export class AppModule {} -
Pipeline declarations on handlers:
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';import { UsePipeline, SkipPipeline } from '@cqrs-ddd/pipeline';import { idempotent } from '@cqrs-ddd/pipeline-idempotency';import { validated } from '@cqrs-ddd/pipeline-zod';@CommandHandler(CreateInvoiceCommand)@UsePipeline(validated(CreateInvoiceSchema), idempotent({ keyFactory }))export class CreateInvoiceHandler implements ICommandHandler<CreateInvoiceCommand> {async execute(command: CreateInvoiceCommand) {// Executed inside the pipeline}} -
Domain Event publication: Subclass
CommandBaseHandlerfrom@cqrs-ddd/core/applicationand pass the injected@nestjs/cqrsEventBus. Whenhandle(command)returns anAggregateRoot, uncommitted domain events are automatically published througheventBus.publishAll()and uncommitted.
Option 2: Migrating off NestJS to @cqrs-ddd/cqrs
Section titled “Option 2: Migrating off NestJS to @cqrs-ddd/cqrs”If your goal is to drop NestJS for a lightweight, framework-free architecture (Express, Fastify, AWS Lambda, Cloudflare Workers), use @cqrs-ddd/cqrs.
pnpm add @cqrs-ddd/cqrs @cqrs-ddd/pipeline @cqrs-ddd/coreWhat Carries Over
Section titled “What Carries Over”The core CQRS API matches @nestjs/cqrs:
@nestjs/cqrs |
@cqrs-ddd/cqrs |
|---|---|
@CommandHandler(Command) |
@CommandHandler(Command) from @cqrs-ddd/cqrs |
@QueryHandler(Query) |
@QueryHandler(Query) from @cqrs-ddd/cqrs |
@EventsHandler(...Events) |
@EventsHandler(...Events) from @cqrs-ddd/cqrs |
ICommandHandler<TCommand> |
ICommandHandler<TCommand> from @cqrs-ddd/cqrs |
IQueryHandler<TQuery> |
IQueryHandler<TQuery> from @cqrs-ddd/cqrs |
IEventHandler<TEvent> |
IEventHandler<TEvent> from @cqrs-ddd/cqrs |
CommandBus, QueryBus, EventBus |
Returned by createCqrs() |
What the Application Does Itself
Section titled “What the Application Does Itself”| NestJS Responsibility | @cqrs-ddd Equivalent |
|---|---|
@Module(), @Injectable(), providers |
A plain composition function that constructs dependencies with new |
NestFactory.create(AppModule) |
const cqrs = createCqrs(options); cqrs.register(...handlers); |
| Application shutdown hooks | await cqrs.close(), then closing database pools and redis connections |
| Controllers, guards, interceptors, pipes | Native HTTP routes and middleware; see HTTP with Express and Fastify |
Request-scoped providers (Scope.REQUEST) |
AsyncLocalStorage via context sources (tenantId, correlationId) |
Complete Plain Node.js Example
Section titled “Complete Plain Node.js Example”import { CommandHandler, createCqrs, type ICommandHandler,} from '@cqrs-ddd/cqrs';import { UsePipeline, LoggingBehavior } from '@cqrs-ddd/pipeline';import { AuditBehavior, type AuditSink, audit } from '@cqrs-ddd/pipeline-audit';
class RegisterUserCommand { constructor(readonly email: string) {}}
@CommandHandler(RegisterUserCommand)@UsePipeline(audit({ action: 'user.registered' }))class RegisterUserHandler implements ICommandHandler<RegisterUserCommand> { constructor(private readonly users: UsersRepository) {}
async execute(command: RegisterUserCommand): Promise<string> { const user = await this.users.create(command.email); return user.id; }}
// Composition Rootexport function buildApplication(users: UsersRepository, auditSink: AuditSink) { const cqrs = createCqrs({ behaviors: [new AuditBehavior(auditSink)], globalBehaviors: { before: [LoggingBehavior] }, });
cqrs.register(new RegisterUserHandler(users));
return cqrs;}Differences in Runtime Behavior
Section titled “Differences in Runtime Behavior”- Strict handler registration: Registering a second handler for the same command or query fails immediately in
cqrs.register(). NestJS silently overrides earlier handlers with the last one registered. - Async missing handler errors: In
@cqrs-ddd/cqrs,commandBus.execute()andqueryBus.execute()reject asynchronously withCommandHandlerNotFoundExceptionorQueryHandlerNotFoundException.@nestjs/cqrsthrows synchronously. - Graceful shutdown:
cqrs.close()awaits all running asynchronous event handlers before resolving. In NestJS,app.close()does not track background event handler completion. - Event error isolation: As in
@nestjs/cqrs, an event handler’s failure goes to theUnhandledExceptionBusand the handler keeps receiving later events;@cqrs-ddd/cqrsalso logs it, or rethrows it withrethrowUnhandled: true. - Not included in
@cqrs-ddd/cqrs: Sagas,ofTypeRxJS pipes,AsyncContext, andEventPublishermerge mechanisms. Aggregates publish events throughCommandBaseHandler.
Choosing Between the Two
Section titled “Choosing Between the Two”- Choose Option 1 (
@cqrs-ddd/nestjs) when working in an existing NestJS codebase, using NestJS microservices or GraphQL, or relying heavily on Nest’s controller ecosystem and DI modules. - Choose Option 2 (
@cqrs-ddd/cqrs) when building greenfield microservices, serverless functions, high-throughput APIs on Fastify, or applications where zero dependency-injection overhead and instant startup are required.