npm package: @raphaabreu/nestjs-zod-cqrs
Description: Zod-validated commands, queries, and events for @nestjs/cqrs
Author: Raphael Lorenzeto de Abreu
Latest version: 1.0.1

# @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

```bash
npm install @raphaabreu/nestjs-zod-cqrs
```

**Peer dependencies:** `@nestjs/cqrs >= 11.0.0`, `zod >= 3.0.0`

## Usage

### Commands

```ts
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

```ts
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

```ts
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

```ts
// 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(),
  }),
}) {}
```

```ts
// 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(),
  }),
) {}
```

```ts
// 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

```ts
// 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

```ts
// 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

```ts
// 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 schema
- **`safeCreate(raw)`** — parse input and return a `SafeParseResult`

### `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 schemas
- **`safeCreate(raw)`** — parse input and return a `SafeParseResult`
- **`output(raw)`** — validate and shape a handler's return value, stripping any fields not in the output schema (throws on failure)
- **`safeOutput(raw)`** — same as `output` but returns a `SafeParseResult` instead of throwing

## License

MIT