khoidang2110

nestjs-clean-hex-cqrs-api-playbook

0
0
# Install this skill:
npx skills add khoidang2110/agent-skills --skill "nestjs-clean-hex-cqrs-api-playbook"

Install specific skill from multi-skill repository

# Description

Use this skill FIRST when implementing or modifying ANY NestJS API. Enforces Clean Architecture + Hexagonal (Ports & Adapters) with CQRS-lite and strict Nx module boundaries.

# SKILL.md


name: nestjs-clean-hex-cqrs-api-playbook
description: Use this skill FIRST when implementing or modifying ANY NestJS API. Enforces Clean Architecture + Hexagonal (Ports & Adapters) with CQRS-lite and strict Nx module boundaries.


NestJS API Playbook

Clean Architecture + Hexagonal (Ports & Adapters) + CQRS-lite

This skill defines the mandatory architecture, layering rules, and step-by-step workflow
for implementing APIs in this repository.

If you are adding or modifying an API endpoint, you MUST follow this file.


TL;DR – Mandatory Flow

Controller (libs/api)
β†’ QueryService / UseCase (libs/application/features)
β†’ inject Port token (libs/application/contracts)
β†’ Adapter + PersistenceModule (libs/persistence/repositories)
β†’ TypeORM entities (libs/persistence/typeorm/entities)

Architecture style:
- Clean Architecture
- Hexagonal (Ports & Adapters)
- CQRS-lite (QueryService vs UseCase)


1. Architecture Enforcement (NON-NEGOTIABLE)

1.1 Nx boundaries

  • ❌ DO NOT relax or bypass @nx/enforce-module-boundaries
  • ❌ DO NOT add exceptions β€œjust to make it work”
  • If a violation occurs:
  • Move the file to the correct layer or
  • Reverse the dependency using a Port + Token

1.2 Composition root

  • apps/api (and libs/api) are composition roots
  • They:
  • assemble modules
  • wire dependencies
  • They must not contain business logic

1.3 Dependency direction

api
↓
application
↓
contracts
↑
persistence

No other direction is allowed.


2. Layers & Allowed Imports

2.1 API layer (libs/api)

Responsible for:
- routing
- guards & roles
- DTO validation
- swagger decorators
- mapping input β†’ QueryService / UseCase

Rules:
- ❌ Must NOT import:
- TypeORM repositories
- entities
- persistence adapters
- βœ… May import:
- application queries/usecases
- shared guards/decorators


2.2 Application layer (libs/application)

Responsible for:
- business flows
- orchestration
- invariants enforcement

Rules:
- βœ… May depend on:
- contracts (ports + tokens)
- shared
- ❌ Must NOT import:
- TypeORM
- DataSource
- persistence adapters/entities
- Only QueryService / UseCase can throw DomainError


2.3 Contracts (libs/application/src/contracts)

Contains:
- Ports (interfaces)
- Tokens
- Shared DTOs (if needed)

Token pattern (canonical):
```ts
export const BALANCE_QUERY_PORT =
'balance/BalanceQueryPort' as const;
Rules:

Application injects by token

Never inject adapters directly

2.4 Persistence (libs/persistence)
Contains:

TypeORM entities

adapters implementing ports

persistence modules that bind tokens

Rules:

Implements ports defined in contracts

One canonical implementation per port

No parallel implementations for the same port

2.5 Shared (libs/shared)
Contains:

guards

decorators (@CurrentUser, @Roles)

DomainError

error codes

response helpers

No business logic here.

  1. Workflow: Adding a New API (COPY THIS)
    Step A β€” Define Port + Token (Contracts)
    Location:

libs/application/src/contracts//ports/
Files:

.query.port.ts (read)

.usecase.port.ts or .command.port.ts (write)

.tokens.ts

Rules:

Application depends on ports, not adapters

Token is injected everywhere

Step B β€” QueryService / UseCase (Application)
Location:

libs/application/src/features//queries/
libs/application/src/features//usecases/
Naming:

GetXxxQueryService

CreateXxxUseCase

UpdateXxxUseCase

Pattern:

@Inject(TOKEN)
private readonly port: XxxPort;
Error handling:

throw new DomainError(ErrorCode.X, 'message');
Barrel export (REQUIRED):

libs/application/src/features//index.ts
Step C β€” Adapter + PersistenceModule
Adapter:

libs/persistence/src/repositories//.adapter.ts
Module:

libs/persistence/src/repositories//.persistence.module.ts
Binding pattern:

providers: [
Adapter,
{ provide: XXX_QUERY_PORT, useExisting: Adapter },
{ provide: XXX_USECASE_PORT, useExisting: Adapter },
],
exports: [XXX_QUERY_PORT, XXX_USECASE_PORT],
Rules:

One adapter may implement multiple ports

Use useExisting

Avoid duplicate implementations

Step D β€” Controller + ApiModule
Controller location:

libs/api/src/controllers///
Role conventions:

user/

admin/

backoffice/

Controller rules:

Guards + roles required

Use @CurrentUser() for identity

Do NOT accept {userId} from params for user APIs

Api module:

libs/api/src/controllers//.module.ts
Imports:

PersistenceModule

Providers:

QueryService / UseCase classes

  1. Naming & Route Conventions
    4.1 Naming
    Query: GetXxxQueryService

UseCase: CreateXxxUseCase, AdjustXxxUseCase

Token: _QUERY_PORT

Module: PersistenceModule

4.2 Routes
User APIs:

/api/v2/user/...
Admin:

/api/v2/admin/...
Backoffice:

/api/v2/backoffice/...
Rules:

❌ No /me for multi-actor resources

/me only for identity/profile

  1. Definition of Done (MANDATORY)
    An API is DONE only when ALL are present:

Controller (guards, roles, swagger)

QueryService / UseCase

Port + Token

Adapter implementation

PersistenceModule binding

ApiModule wiring

Nx boundaries clean (no violations)

  1. Error Conventions (DomainError)
    6.1 Rules
    Application layer throws DomainError ONLY

Application NEVER throws HttpException

Adapter:

expected business error β†’ map to DomainError

unexpected error β†’ throw raw (500)

6.2 Error code naming
UPPER_SNAKE_CASE

Prefixed by feature

Examples:

BALANCE_NOT_FOUND

SUPPORT_TICKET_CLOSED

FILE_TYPE_NOT_ALLOWED

  1. Transactions
    Use typeorm-transactional if enabled

Avoid manual nested transactions

Transaction boundary belongs in UseCase

  1. Events
    In-memory (RAM)
    Use for non-critical internal flows:

UserRegistered

TestEmail

Outbox (DB-backed)
Use for reliable, cross-service events:

PaymentSucceeded

ItemsReceived

ImportReceiptQuantityRecorded

Rule:

If it must not be lost β†’ Outbox

If best-effort β†’ In-memory

Final Rule
If you are unsure:

Copy the Balance feature structure exactly.
Deviation requires architectural justification.

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.