---
name: nestjs-cqrs-module
description: "Architecture guide for NestJS modules with CQRS + Event Sourcing. Use when creating modules, adding queries/commands/sagas, or scaffolding. Triggers on \"new module\", \"create module\", \"CQRS pattern\"."
---

# NestJS CQRS Module Pattern

Architecture guide for building NestJS modules with CQRS, Event Sourcing, and clean separation of concerns. Extracted from production module `sub_payment-th3`.

---

## When to Use This Skill

- Creating a new NestJS sub-module
- Adding queries, commands, sagas to existing module
- Structuring models with business logic encapsulation
- Building external API integrations (proxy pattern)
- Mapping internal models to API responses

## When NOT to Use This Skill

- Simple CRUD without business logic (use basic NestJS controller + service)
- Frontend/React code
- Database migration scripts

---

## Module Directory Structure

```
sub_{module-name}/
├── model/                    # Domain models + business logic
│   ├── {module}.model.ts                 # Core domain model (fromDatabase)
│   ├── {module}-{context}.model.ts       # Context-specific logic (validate, map)
├── queries/                  # CQRS Read operations
│   ├── get-{resource}-by-{criteria}.query.ts
│   └── index.ts              # export const QueryHandlers = [...]
├── commands/                 # CQRS Write operations
│   ├── {action}-{resource}.command.ts
│   └── index.ts              # export const CommandHandlers = [...]
├── sagas/                    # Event-driven orchestration
│   ├── {domain}.saga.ts
│   └── index.ts              # export const SagaHandlers = [...]
├── proxy/                    # External HTTP calls
│   ├── {domain}.proxy.ts
│   └── index.ts              # export const ProxyHandlers = [...]
├── repositories/             # Data access (raw SQL)
│   └── {module}.repository.ts
├── dto/                      # Input validation
│   └── {action}-{module}.dto.ts
├── mapper/                   # Response transformation
│   └── {module}-{context}.mapper.ts
├── events/                   # Event definitions
├── {module}.module.ts        # DI wiring
├── {module}.service.ts       # Orchestration (thin logic)
├── {module}.controller.ts    # HTTP endpoints (thin)
└── {module}.variables.ts     # Enums + constants
```

---

## Core Rules

### 1. Controller: Thin HTTP Layer

Controller only does: route + validate DTO + call service + map response.

```typescript
@Post('request-payment')
@HttpCode(200)
async requestPayment(@Body() dto: RequestPaymentDto) {
  const result = await this.service.requestPayment(dto);
  return RequestMapper.toResponse(result);
}
```

### 2. Service: Orchestration Only

Service coordinates steps, does NOT contain business logic. Delegate to models.

```typescript
async requestPayment(dto: RequestPaymentDto) {
  // Validate via model
  const secret = RequestModel.validateSourceSystem(sourceSystem);

  // Parallel data fetching
  const [setting, program, existing] = await Promise.all([
    this.settingService.getOne(SETTING_KEYS.PAYMENT_AMOUNT),
    this.queryBus.execute(new GetProgramQuery(dto.code)),
    this.queryBus.execute(new GetExistingQuery(dto.id)),
  ]);

  // Validate via model
  RequestModel.validateMinAmount(dto.amount, setting?.value);

  // Dispatch command
  return this.commandBus.execute(new CreateCommand(data));
}
```

### 3. Model: Business Logic Encapsulation

Models contain ALL validation, mapping, and domain logic as **static methods**.

```typescript
export class PaymentRequestModel {
  // Validation
  static validateSourceSystem(source: any): string { ... }
  static validateMinAmount(amount: number, setting: any): void { ... }
  static validateDateRange(from: string, to: string): void { ... }

  // State checks
  static isAlreadyPaid(ps: PaymentSourceModel, tx: TransactionModel | null): boolean { ... }
  static isHashChanged(ps: PaymentSourceModel, hash: string): boolean { ... }

  // Data transformation
  static toCreateData(dto, programId, batchId) { ... }
  static toUpdateData(existingId, dto, programId, batchId) { ... }

  // Hash
  static async verifyHash(dto, secret): Promise<boolean> { ... }
}
```

### 4. Query/Command: CQRS Pattern

**Query** (READ): class + handler in same file.

```typescript
// get-{resource}-by-{criteria}.query.ts
export class GetResourceByCriteriaQuery {
  constructor(public readonly criteria: string) {}
}

@QueryHandler(GetResourceByCriteriaQuery)
export class GetResourceByCriteriaQueryHandler
  implements IQueryHandler<GetResourceByCriteriaQuery>
{
  constructor(private readonly repo: ModuleRepository) {}

  async execute(query: GetResourceByCriteriaQuery) {
    const raw = await this.repo.getByCriteria(query.criteria);
    return raw ? ResourceModel.fromDatabase(raw) : null;
  }
}
```

**Command** (WRITE): same pattern, use `@CommandHandler`.

**Index file** exports array for module registration:

```typescript
// queries/index.ts
export const QueryHandlers = [
  GetResourceByCriteriaQueryHandler,
  GetAnotherQueryHandler,
];
```

### 5. Repository: Raw SQL + Parameterized Queries

```typescript
@Injectable()
export class ModuleRepository {
  constructor(
    @PgSQLConnection(CONNECTION_STRING_DEFAULT)
    private readonly pg: PgSQLConnectionPool,
  ) {}

  async getByCriteria(criteria: string) {
    const sql = `
      SELECT * FROM table_name
      WHERE column = $1 AND status != $2
      LIMIT 1;
    `;
    const result = await this.pg.execute(sql, [criteria, Status.DELETED]);
    return result.rows[0] || null;
  }
}
```

### 6. Mapper: Response Transformation

Always return ALL fields. Use `null` instead of omitting fields.

```typescript
export class StatusMapper {
  static toResponse(data: { status: string; source: Model; tx?: any }): StatusResponse {
    return {
      status: _.get(data, 'status', null),
      payment_code: _.get(data, 'source.payment_code', null),
      amount: _.get(data, 'tx.amount', null),
      paid_at: _.get(data, 'tx.time_update', null),
    };
  }
}
```

### 7. Variables: Module-Specific Enums

Define enums specific to the module context. Do NOT reuse enums from other modules for response types.

```typescript
// Module-specific status (not tied to TransactionStatus)
export enum ModuleCheckStatus {
  SUCCESS = 'SUCCESS',
  PENDING = 'PENDING',
  FAILED = 'FAILED',
}
```

### 8. DTO: Validation with class-validator

```typescript
export class RequestDto {
  @IsString()
  @IsNotEmpty()
  @ApiProperty({ example: 'DTTX' })
  source_system: string;

  @IsNumber()
  @ApiProperty({ example: 5000000 })
  total_amount: number;

  @IsDateString()
  @ApiProperty({ example: '2024-01-01' })
  from_date: string;
}
```

### 9. Proxy: External HTTP with Retry

```typescript
@Injectable()
export class WebhookProxy {
  constructor(private readonly httpService: HttpService) {}

  async call(url: string, payload: any, maxRetries = 3): Promise<any> {
    const { data } = await firstValueFrom(
      this.httpService.post(url, payload, { timeout: 10000 }).pipe(
        tap({ error: (e) => this.logger.warn(`Retry: ${e.message}`) }),
        retry({ count: maxRetries, delay: 1000 }),
        catchError((error) => throwError(() => error)),
      ),
    );
    return data;
  }
}
```

### 10. Saga: Event-Driven Workflow

```typescript
@Injectable()
export class WebhookSaga {
  @Saga()
  onPaymentSuccess = (events$: Observable<any>): Observable<ICommand> => {
    return events$.pipe(
      ofType(PaymentSuccessEvent),
      filter(event => !!event.paymentSource?.th3_info),
      map(event => new SendWebhookCommand(event.data)),
    );
  };
}
```

### 11. Module Wiring

```typescript
@Module({
  imports: [
    CqrsModule,
    HttpModule,
    PostgresModule.forFeature(CONNECTION_STRING_DEFAULT),
    // External module dependencies
  ],
  controllers: [ModuleController],
  providers: [
    ModuleService,
    ModuleRepository,
    ...QueryHandlers,      // Spread from index.ts
    ...CommandHandlers,
    ...SagaHandlers,
    ...ProxyHandlers,
  ],
})
export class SubModuleModule {}
```

---

## Data Flow

```
HTTP Request
  -> Controller (validate DTO, delegate)
    -> Service (orchestrate steps, parallel fetch)
      -> QueryBus/CommandBus
        -> Handler (execute, transform)
          -> Repository (raw SQL)
          -> Model.fromDatabase() (map to domain)
      -> Model.validate*() (business rules)
    -> Mapper.toResponse() (shape output)
  -> HTTP Response

Event (async)
  -> Saga (filter, map to command)
    -> CommandHandler
      -> Proxy (external HTTP with retry)
```

---

## Naming Conventions

| Type | Pattern | Example |
|------|---------|---------|
| Model | `{Domain}Model` | `PaymentSourceTH3Model` |
| Query | `Get{Resource}By{Criteria}Query` | `GetPaymentSourceBySourceAndCodeQuery` |
| Command | `{Action}{Resource}Command` | `CreatePaymentSourceTH3Command` |
| Handler | `{Query/Command}Handler` | `CreatePaymentSourceTH3CommandHandler` |
| Saga | `{Domain}Saga` | `TH3WebhookSaga` |
| Proxy | `{Domain}Proxy` | `TH3WebhookProxy` |
| Repository | `{Module}Repository` | `SubPaymentTh3Repository` |
| Mapper | `{Module}{Context}Mapper` | `SubPaymentTh3StatusMapper` |
| DTO | `{Action}{Module}Dto` | `RequestPaymentTH3Dto` |
| Enum | `{Prefix}{Type}` | `TH3CheckStatus` |
| File | kebab-case | `get-payment-source-by-code.query.ts` |

---

## Anti-Patterns (Avoid)

- **Business logic in service** — Move to model static methods
- **Optional response fields** — Always return all fields, use `null`
- **Reusing enums from other modules** for response status — Create module-specific enums
- **Sequential queries that can run parallel** — Use `Promise.all`
- **Raw data from query handler** — Always map through `Model.fromDatabase()`
- **Fat controller** — Controller should only: validate DTO + call service + map response
- **Direct repo calls in service** — Use QueryBus/CommandBus for CQRS consistency
