A DDD example
packages/ddd-core and the api example demonstrate Domain-Driven Design with @nestjs-pipeline.
packages/ddd-core — framework-neutral DDD support
Section titled “packages/ddd-core — framework-neutral DDD support”The @cqrs-ddd/core package has no NestJS dependency and provides four explicit entry points:
/domain— aggregate/event/error primitives;/application— CQRS base classes and repository/cache ports;/persistence— persistence decorators/adapters/helpers;/http— HTTP status mapping for its own errors.
Domain and application code should use the narrow entry points rather than the compatibility root barrel.
| Export | Layer | Description |
|---|---|---|
RootEntity |
/domain |
Abstract base entity with UUID v7 identity, createdAt/updatedAt lifecycle, accessor mappings, and mutation tracking |
RootEntitySnapshot |
/domain |
Interface for serializing/rehydrating entities |
DomainException |
/domain |
Abstract base class for framework-agnostic domain invariant exceptions |
DomainEvent |
/domain |
Abstract base class for domain events (carries a UUID v7 id) |
RootDomainEvent |
/domain |
Domain event with detached immutable payload and event-time aggregate ID/version |
@ApplyMutation() |
/domain |
Completes a domain mutation: calls onUpdate() and records events from the result |
@Mutable() |
/domain |
Declares an aggregate field as patchable through applyPatch(...) |
CommandBaseHandler |
/application |
Base CQRS command handler that dispatches and clears uncommitted events |
ICommandRepository |
/application |
Port for write repositories |
IQueryRepository |
/application |
Port for read repositories |
ICache<T> |
/application |
Interface for cache providers (get, set, delete) |
@Cache() |
/persistence |
Decorator for save() — write-through cache on writes, evict on delete, explicit keys |
@FromCache() |
/persistence |
Decorator for find() — read-through cache with fail-closed semantics and hydration |
MikroORM adapters, such as AggregateRepository, MikroOrmCache and UnixTimestampType,
live in @cqrs-ddd/mikro-orm.
Import domain primitives in your domain layer:
import { ApplyMutation, DomainException, RootDomainEvent, RootEntity } from '@cqrs-ddd/core/domain';api — Full Working Application
Section titled “api — Full Working Application”The api/ directory contains a complete working application:
cd apipnpm installpnpm build # build workspace dependenciescp .env.example .env # create local environment file (edit as needed)pnpm db:migrate # apply schema + data migrations (idempotent)pnpm dev # build, then rebuild and restart on source changesConfigure the database via environment variables (defaults to a local file):
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
file:src/persistence/local.db |
libSQL URL, used unchanged for one tenant |
SQLITE_TENANTS |
(none) | Additional libSQL tenant names; local files get a tenant suffix |
SQLITE_DATABASE_TEMPLATE |
(none) | URL containing {tenant}; required for multiple remote tenants |
AUTH_TOKEN |
(none) | Auth token for libSQL remote databases (e.g. Turso) |
DB_ENGINE=postgres switches to PostgreSQL with a schema per tenant; the
api README lists every variable.
Both persistence engines require x-tenant-schema on routed HTTP requests.
PostgreSQL selects a schema; libSQL selects the corresponding database.
CRUD operations:
# Log in as the seeded admin, then copy `accessToken` from the JSON response.# The refresh token arrives as an HttpOnly cookie; POST /auths/refresh exchanges it.curl -X POST http://localhost:3000/auths/login -c cookies.txt \ -H 'Content-Type: application/json' \ -H 'x-tenant-schema: tenant' \ -d '{"email":"alice+tenant@seed.local","code":"secret-code"}'export TOKEN='<accessToken from login response>'
# Create a usercurl -X POST http://localhost:3000/users \ -H 'Content-Type: application/json' \ -H 'x-tenant-schema: tenant' \ -H "Authorization: Bearer $TOKEN" \ -H 'x-correlation-id: demo-123' \ -d '{"name": "Aristotelis", "email": "aristotelis@example.com"}'
# Get all userscurl -X GET http://localhost:3000/users \ -H 'x-tenant-schema: tenant' \ -H "Authorization: Bearer $TOKEN"
# Get by IDcurl http://localhost:3000/users/<id> \ -H 'x-tenant-schema: tenant' \ -H "Authorization: Bearer $TOKEN"
# Updatecurl -X PATCH http://localhost:3000/users/<id> \ -H 'Content-Type: application/json' \ -H 'x-tenant-schema: tenant' \ -H "Authorization: Bearer $TOKEN" \ -d '{"name": "NewName"}'
# Delete a usercurl -X DELETE http://localhost:3000/users/<user-id> \ -H 'x-tenant-schema: tenant' \ -H "Authorization: Bearer $TOKEN"
# Run with Fastify adapterADAPTER=fastify pnpm startWhat it demonstrates:
- Global + per-handler pipeline behaviors
- Decoupled domain invariants with framework-agnostic
DomainException& presentation-boundaryDomainExceptionFilter(mapping to 400, 409, 422) - Clean Architecture persistence repository boundaries: CQRS handlers inject exclusively
ICommandRepositoryandIQueryRepository, completely decoupled from ORM/database client classes (zeroMIKRO_ORM_CLIENTleakage in handlers) - Persistent token revocation on logout via
RevokeAuthCommand, per-request permission loading inCaslPermissionSource, and explicit principal classification - Optimistic concurrency with aggregate version tracking on
UserandRoleentities. Persistence adapters translate driver/ORM conflict diagnostics into the transport-neutralConcurrencyConflictError; the HTTP presentation filter maps that error to409 Conflict. - Injectable
CaslAuthorizerin CQRS command and query handlers:authorizebefore writes,projectfor read models and responses - Per-handler CASL requirements declared with
requires(...) - MikroORM-backed CASL permission source (roles, per-user grants and denials)
- Official MikroORM
accessor: trueentity schemas bridging private aggregate fields to public accessors without TypeScript bypasses - Decoupled CQRS caching architecture with collision-safe key derivation (
cacheKey), fail-fast handler templates (cacheKeyTemplate), and static aggregate naming (User.aggregateName) - Versioned database migrations with tracking (
mikro_orm_migrationstable) - Zod-parsed/validated commands and queries via
createCommand()andcreateQuery()exposing Standard Schema (['~standard']) metadata - Controller-level schema validation through Nest’s
StandardSchemaValidationPipewithzodBadRequest - Zod transform mappers (DTO → Command mapping)
- OpenTelemetry tracing with
TraceBehaviorand metrics withMetricsBehavior - Command- and event-scoped
DeadLetterBehaviorsending failed executions to a BullMQdead-lettersqueue for inspection and replay (restricted to mutating command and event failures, excluding read queries and validation errors, withUserCreatedHandleropting into{ rethrow: false }only after successful delivery); transport failures are logged and preserve the original handler error - Per-handler
RateLimitBehaviorthrottlingCreateUserHandlerto 5 registrations / 60s per email (in-memory limiter), withRateLimitExceededFiltermapping breaches to HTTP 429 +Retry-After - Per-handler
AuditBehaviorrecording the sensitiveuser.deleteaction (actor, outcome, duration, redacted payload) to the defaultLogAuditSink, with the actor resolved from the request-scoped session - Per-handler
IdempotencyBehavioratomically excluding concurrent duplicates forCreateUserHandlerper tenant + principal + email and replaying completed successful responses; with the defaultreleaseOnError: true, a failed execution releases the key so a later retry may execute again.IdempotencyConflictFiltermaps in-flight duplicates to HTTP 409 and payload-mismatched key reuse to HTTP 422 - DDD-style
UserandRoleentities built onddd-coreprimitives (RootEntity,RootDomainEvent) - MikroORM (libSQL and PostgreSQL drivers) persistence with multi-tenant database/schema isolation
- Pluggable
ICache<T>—MikroOrmCache(MikroORM-backed, TTL-aware) orMemoryCacheswapped via a single provider token - Correlation ID propagation across HTTP middleware, handlers, processors, and events
- Express and Fastify adapter support with secure session cookie and Bearer/API-key authentication