@raphaabreu/nestjs-zod-cqrs
v1.0.1
Published
Zod-validated commands, queries, and events for @nestjs/cqrs
Readme
@raphaabreu/nestjs-zod-cqrs
Zod-validated commands, queries, and events for @nestjs/cqrs. Define your CQRS messages as classes with built-in runtime validation — input is parsed on construction, output is validated and stripped to the declared shape so handlers can work with rich internal models while only exposing lean, well-defined results.
Installation
npm install @raphaabreu/nestjs-zod-cqrsPeer dependencies: @nestjs/cqrs >= 11.0.0, zod >= 3.0.0
Usage
Commands
import { z } from 'zod';
import { defineZodCommand } from '@raphaabreu/nestjs-zod-cqrs';
class PlaceOrderCommand extends defineZodCommand({
input: z.object({
productId: z.string(),
quantity: z.number().int().positive(),
}),
output: z.object({
orderId: z.string(),
createdAt: z.string().datetime(),
}),
}) {}
// Validated construction — throws on invalid input
const cmd = new PlaceOrderCommand({ productId: 'p1', quantity: 3 });
// Shape handler return value — validates and strips extra fields (throws on failure)
const output = PlaceOrderCommand.output({ orderId: 'o1', createdAt: '2024-01-01T00:00:00Z', ...internalFields });
// Safe variant — returns SafeParseResult instead of throwing
const outputResult = PlaceOrderCommand.safeOutput({
orderId: 'o1',
createdAt: '2024-01-01T00:00:00Z',
...internalFields,
});
if (outputResult.success) {
console.log(outputResult.data.orderId);
}Events
import { z } from 'zod';
import { defineZodEvent } from '@raphaabreu/nestjs-zod-cqrs';
class OrderPlacedEvent extends defineZodEvent(
z.object({
orderId: z.string(),
amount: z.number().positive(),
}),
) {}
// Validated construction — throws on invalid input
const event = new OrderPlacedEvent({ orderId: 'abc', amount: 42 });
// Safe construction — returns SafeParseResult
const result = OrderPlacedEvent.safeCreate({ orderId: 'abc', amount: 42 });
if (result.success) {
console.log(result.data.orderId);
}Queries
import { z } from 'zod';
import { defineZodQuery } from '@raphaabreu/nestjs-zod-cqrs';
class GetOrderQuery extends defineZodQuery({
input: z.object({
orderId: z.string(),
}),
output: z.object({
orderId: z.string(),
status: z.enum(['pending', 'shipped', 'delivered']),
}),
}) {}
const query = new GetOrderQuery({ orderId: 'abc' });
// Shape handler return value — validates and strips extra fields
const output = GetOrderQuery.output({ orderId: 'abc', status: 'shipped', ...internalFields });Real-world example
Definitions
// place-order.command.ts
import { z } from 'zod';
import { defineZodCommand } from '@raphaabreu/nestjs-zod-cqrs';
export class PlaceOrderCommand extends defineZodCommand({
input: z.object({
productId: z.string(),
quantity: z.number().int().positive(),
}),
output: z.object({
orderId: z.string(),
createdAt: z.string().datetime(),
}),
}) {}// order-placed.event.ts
import { z } from 'zod';
import { defineZodEvent } from '@raphaabreu/nestjs-zod-cqrs';
export class OrderPlacedEvent extends defineZodEvent(
z.object({
orderId: z.string(),
productId: z.string(),
quantity: z.number(),
amount: z.number(),
}),
) {}// get-order.query.ts
import { z } from 'zod';
import { defineZodQuery } from '@raphaabreu/nestjs-zod-cqrs';
export class GetOrderQuery extends defineZodQuery({
input: z.object({
orderId: z.string(),
}),
output: z.object({
orderId: z.string(),
productId: z.string(),
quantity: z.number(),
status: z.enum(['pending', 'shipped', 'delivered']),
}),
}) {}Controller
// orders.controller.ts
@Controller('orders')
export class OrdersController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
async placeOrder(@Body() body: unknown) {
// Input is validated here — throws if body doesn't match the schema
const command = new PlaceOrderCommand(body as any);
return this.commandBus.execute(command);
}
@Get(':id')
async getOrder(@Param('id') id: string) {
const query = new GetOrderQuery({ orderId: id });
return this.queryBus.execute(query);
}
}Command handler
// place-order.handler.ts
@CommandHandler(PlaceOrderCommand)
export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
constructor(
private readonly orderRepo: OrderRepository,
private readonly eventBus: EventBus,
) {}
async execute(command: PlaceOrderCommand) {
// command.productId and command.quantity are already validated
const order = await this.orderRepo.create({
productId: command.productId,
quantity: command.quantity,
});
// Publish a validated event
this.eventBus.publish(
new OrderPlacedEvent({
orderId: order.id,
productId: order.productId,
quantity: order.quantity,
amount: order.amount,
}),
);
// order has many internal fields (updatedAt, version, internalNotes, etc.)
// .output() strips everything not in the output schema
return PlaceOrderCommand.output(order);
}
}Query handler
// get-order.handler.ts
@QueryHandler(GetOrderQuery)
export class GetOrderHandler implements IQueryHandler<GetOrderQuery> {
constructor(private readonly orderRepo: OrderRepository) {}
async execute(query: GetOrderQuery) {
// The repo returns a fat internal model with audit fields, soft-delete flags, etc.
const order = await this.orderRepo.findById(query.orderId);
// .output() validates and strips it down to just { orderId, productId, quantity, status }
return GetOrderQuery.output(order);
}
}API
defineZodEvent(schema)
Returns a base class with:
new(input)— parse input and return a class instance (throws on failure)InputSchema— the Zod schemasafeCreate(raw)— parse input and return aSafeParseResult
defineZodCommand({ input, output }) / defineZodQuery({ input, output })
Returns a base class extending Command<O> / Query<O> with:
new(input)— parse input and return a class instance (throws on failure)InputSchema/OutputSchema— the Zod schemassafeCreate(raw)— parse input and return aSafeParseResultoutput(raw)— validate and shape a handler's return value, stripping any fields not in the output schema (throws on failure)safeOutput(raw)— same asoutputbut returns aSafeParseResultinstead of throwing
License
MIT
