Bloques de construcción de Domain-Driven Design para NestJS.
Agregados que recolectan sus propias reglas rotas, value objects que se validan solos y seguimiento de estado — sobre @nestjs/cqrs.
Inicio rápido · Preguntas frecuentes · Los cuatro paquetes · Colaborar · El CLI
npm install @nestjslatam/ddd-lib @nestjs/cqrs@nestjs/cqrs no es opcional — DddAggregateRoot extiende su AggregateRoot. La lista completa de dependencias par está en Requisitos.
import {
DddAggregateRoot,
NumberValueObject,
AbstractRuleValidator,
IdValueObject,
} from '@nestjslatam/ddd-lib';
// Cada regla vive en su propia clase, así se puede probar por separado.
class PriceRule extends AbstractRuleValidator<Price> {
addRules(): void {
if (this.subject.getValue() <= 0) {
this.addBrokenRule('value', 'El precio debe ser mayor que cero');
}
}
}
export class Price extends NumberValueObject {
static create(value: number): Price {
const price = new Price(value);
if (!price.isValid) {
throw new Error(price.brokenRules.getBrokenRules()[0].message);
}
return price;
}
override addValidators(): void {
super.addValidators(); // la base registra reglas reales aquí — encadena siempre
this.validatorRules.add(new PriceRule(this));
}
}
// El agregado carga las invariantes que abarcan más de un value object.
export class Product extends DddAggregateRoot<Product, IProductProps> {
private constructor(props: IProductProps, id?: IdValueObject) {
super(props, { id });
this.trackingState.markAsNew();
}
static create(name: Name, price: Price): Product {
const product = new Product({ name, price });
if (!product.isValid) {
// La validación RECOLECTA reglas, nunca lanza. Si te saltas esta
// comprobación, create() devuelve tan tranquilo un objeto que incumple
// sus propias invariantes.
throw new Error(
product.brokenRules
.getBrokenRules()
.map((r) => r.message)
.join(', '),
);
}
return product;
}
protected override addValidators(): void {
this.validators.add(new ProductRule(this));
}
}Lo que eso te da, y dónde se detecta cada fallo:
| Entrada | Resultado | Lo detecta |
|---|---|---|
Price.create(49.99) |
válido | — |
Price.create(0) |
value must be a positive number (greater than zero) |
el validador base de NumberValueObject |
precio 2_000_000 |
Price must be less than 1000000 |
el agregado, ProductRule |
Fíjate en la segunda fila: El precio debe ser mayor que cero nunca se disparó. super.addValidators() ya había registrado la regla de número positivo de la base, que atrapó el 0 primero. Quita ese super y ambas reglas desaparecen en silencio — sin error, con el valor inválido aceptado.
Esto no está copiado de memoria. El código de arriba vive en libs/ddd/src/readme-example.spec.ts, que comprueba las tres filas más la forma del getter, y CI lo ejecuta en cada push. Los ejemplos a los que sustituyó tenían siete errores de tipos y nunca habían compilado contra ninguna versión publicada — porque nadie los ejecutaba.
Important
Dónde está esta librería, en números. La 4.0.0 es la primera versión con una batería de pruebas que cubre las clases que realmente extiendes. Antes, once de los doce ficheros del núcleo — incluidos DddAggregateRoot y DddValueObject — no tenían ni un test, y escribir esas pruebas destapó 34 defectos, ocho graves: un agregado que fallaba la validación no podía volver a ser válido nunca, clone() devolvía un alias en lugar de una copia, y todas las opciones de StringValueObject se ignoraban en silencio.
La cobertura pasó del 58,4 % al 98,6 %, y las pruebas de 308 a 1017. Así que la afirmación honesta no es «no lo uses» ni «es estable», sino ésta:
| Probado | Las bases de agregado y value object, la validación, las reglas rotas, el seguimiento de estado y la identidad. Cubiertas y fijadas por pruebas de regresión. |
| Recién cubierto, menos rodado en producción | Transiciones de estado, eventos de dominio y enums — sin pruebas hasta la 4.0.0, así que los tests son nuevos aunque el código no lo sea. |
| El riesgo real | Cambios de API, no de corrección. La 4.0.0 cambió el comportamiento observable en ocho sitios y el compilador no detecta ninguno. |
Clava una versión exacta. La API se estabiliza a partir de la 4.0.0: ningún cambio incompatible se publicará sin un ciclo de obsolescencia donde sea técnicamente posible. Esa promesa se gana a lo largo de un ciclo de versiones, no anunciándola — júzgala en la 4.1.0.
No instales la 2.0.0 ni la 2.1.0. Ambas están marcadas como obsoletas en npm por reventar al importarlas, y un rango ^2.0.0 todavía resuelve a ellas.
A la 3.0.0 — un solo cambio, y el compilador encuentra todos los sitios:
- if (!aggregate.isValid()) {
+ if (!aggregate.isValid) {isValid era un método en los agregados y un getter en los value objects — el mismo nombre con dos formas, que es exactamente cómo un guard como if (!aggregate.isValid) podía leerse como una Function siempre verdadera y no dispararse jamás. Ahora los dos son getters. TypeScript avisa con TS6234; para quien consuma desde JavaScript, npx ddd validate señala cada llamada leyendo cómo lo declara tu versión instalada.
A la 4.0.0 — no hay nada que buscar, porque el compilador no detecta nada de esto. Cambiaron ocho comportamientos, en el orden en que probablemente te afecten:
- Quita cualquier apaño con
brokenRules.clear()antes devalidate(). Ahora se limpia solo. - Revisa todo lo que lea
clone()/getCopy(). Devuelven una copia de verdad; si dependías de que la copia compartiera estado, eso era el bug. Vuelve a suscribir en la copia los manejadores de cambio de propiedad. - Pasa a minúsculas los UUID que tengas guardados, o espéralos en minúsculas al leer.
IdValueObjectlos canonicaliza, así que el mismo UUID en dos capitalizaciones es por fin una sola identidad. - Vuelve a comprobar las subclases de
StringValueObjectque pasen opciones.allowEmpty,trimWhitespace,minLengthymaxLengthse ignoraban y ahora se aplican, así que valores que antes pasaban pueden fallar. IdValueObject.setValue()lanza ante cualquier cosa que no sea un UUID, en vez de aceptarlo en silencio.DddEnum.getAll()devuelve un array nuevo en cada llamada;getAll() === getAll()ya no es cierto.- Un comparador de estado personalizado recibe
(definedState, queryState)en todas las llamadas. - La detección de cambios anidados ahora se dispara para los objetos propios de esta librería, así que los repositorios pueden ver escrituras que antes se saltaban.
El registro de cambios explica el razonamiento de cada uno.
DddAggregateRoot extiende el AggregateRoot de @nestjs/cqrs y trae cableados cuatro colaboradores que si no tendrías que escribir a mano: BrokenRulesManager (recolección de errores), ValidatorRuleManager (registro de reglas), TrackingStateManager (nuevo / sucio / limpio) y StateTransitionManager (una máquina de estados). Más la identidad de IdValueObject, un equals consciente del prototipo y toPlainObject.
npx ddd list imprime el inventario completo leyendo las declaraciones de la versión que tengas instalada, así que no puede quedarse obsoleto como sí le pasaría a una tabla escrita aquí.
| Paquete | Instálalo cuando | Versión |
|---|---|---|
ddd-lib |
Siempre. Ésta es la librería. Se construye desde libs/ddd en este repositorio. |
4.0.0 |
ddd-cli |
Como dependencia de desarrollo, para andamiar y auditar. No es de ejecución. | 0.4.0 |
ddd-valueobjects |
Quieres tipos ya hechos de email, teléfono, dinero o documentos de identidad. | 1.3.0 |
ddd-es-lib |
Vas a hacer event sourcing sobre MongoDB. Exige mongoose. |
1.5.1 |
@nestjslatam/ddd-cli lee los ficheros .d.ts del ddd-lib instalado en tu proyecto usando la API del compilador de TypeScript — así que describe tu versión, y no aquella contra la que se construyó.
npx ddd list # cada estereotipo, agrupado por cómo se usa
npx ddd new value-object Sku # andamiaje; no escribe nada hasta que confirmas
npx ddd validate # audita contra el idioma de la libreríavalidate aplica cuatro reglas, cada una un error que esta librería hace fácil y silencioso:
| Regla | El error que atrapa |
|---|---|
no-subclass-state-in-add-validators |
El constructor base llama a addValidators() antes de que se ejecute el cuerpo del tuyo. Leer un campo que asignas ahí lanza en cada construcción — exactamente como NumberValueObject se publicó roto durante dos versiones. |
super-add-validators |
Un override que no encadena tira los validadores reales de la base, y los valores inválidos pasan sin error. |
factory-checks-validity |
Un create() que se salta la comprobación de isValid devuelve objetos que incumplen sus propias invariantes. |
handler-commits-events |
Sólo mergeObjectContext(...).commit() despacha los eventos de dominio. Sin eso el comando triunfa y todos los manejadores se saltan en silencio. |
También corre como servidor MCP, así que Claude Code, Codex o Cursor lo manejan con su propio modelo y sin clave de API:
claude mcp add ddd -- npx -y @nestjslatam/ddd-cli mcpsrc/ es un ejemplo de Pedidos y Productos que consume la librería. No se publica.
npm install
npm run start:dev # :3000, Swagger en /apiUn recorrido completo, cubierto por test/app.e2e-spec.ts:
POST /products 201 { "id": "..." }
POST /orders 201 un DRAFT vacío
POST /orders/:id/items 200
PATCH /orders/:id/items/:productId 200
POST /orders/:id/confirm 200Dos clases de error, dos respuestas — y la distinción es el motivo de todo esto:
| Petición | Respuesta | Lo detecta |
|---|---|---|
price: "cuarenta" |
400 nombrando el campo | ValidationPipe, antes de que el dominio lo vea |
price: 0 |
422 con las reglas rotas | PriceRangeValidator, dentro del agregado |
{ ..., isAdmin: true } |
201, la clave extra se quita | whitelist: true |
// POST /products { "price": 0 } -> 422
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Price is invalid",
"brokenRules": [
{
"property": "value",
"message": "Price must be greater than zero",
"severity": "Error"
}
]
}Un tipo equivocado es estructura y nunca llega al dominio. Un valor equivocado es significado, y sólo el agregado puede juzgarlo. DomainExceptionFilter mapea todo el vocabulario del dominio:
| Excepción | Estado | Se llega desde |
|---|---|---|
BrokenRulesException |
422 |
quantity: 0, price: 0 — rechazados por una invariante |
ArgumentNullException |
400 |
un valor obligatorio ausente o en blanco |
InvalidFormatException |
400 |
un id que no es UUID, un estado fuera del enum |
InvalidStateTransitionException |
409 |
DRAFT → SHIPPED |
InvalidOperationException |
409 |
confirmar un pedido sin artículos |
NotFoundException de Nest |
404 |
un artículo que el pedido no contiene |
Todo lo que no sea una excepción de dominio se deja deliberadamente como 500. Dos throw en money.vo.ts siguen así a propósito, con un comentario que lo explica: ningún endpoint puede pedir una división por cero, así que llegar a una significa un bug en este código, y disfrazar un fallo de error del cliente lo esconde.
Los repositorios son en memoria por diseño: el ejemplo trata sobre el dominio, no sobre una base de datos. Implementa el contrato del repositorio contra tu propio almacén.
Cuatro paquetes, ¿cuál instalo realmente?
@nestjslatam/ddd-lib, y sólo ése, salvo que necesites específicamente alguno de los otros. ddd-cli es dependencia de desarrollo. Mira la tabla de arriba.
¿Funciona con mi versión de NestJS y de Node?
Declarado: NestJS 10 u 11, Node >=20.11. En la práctica sólo se ejercita NestJS 11.2.3 — CI varía Node (18, 20, 22) y nunca varía NestJS, así que trata NestJS 10 como no probado, no como soportado.
¿Qué me da DddAggregateRoot frente a escribir mi propia clase base?
Los cuatro gestores que se listan en Qué obtienes, ya cableados a @nestjs/cqrs. Ten en cuenta que StateTransitionManager es la pieza menos rodada — el ejemplo de este repositorio no lo usa, se escribe su propio canTransitionTo.
¿Está listo para producción? ¿Qué versión clavo?
Sí para el modelo de dominio, con una versión exacta clavada — y eso es un cambio respecto a lo que decía este README antes de la 4.0.0.
La 4.0.0 es la primera versión cuyas clases base tienen pruebas: 1017 de ellas, 98,6 % de líneas. Llegar ahí destapó 34 defectos, así que la advertencia anterior estaba ganada, no era relleno. Lo que queda no es riesgo de corrección sino cambio de API — la 4.0.0 movió comportamiento en ocho sitios que el compilador no ve. Clava exacto, lee la migración, y juzga la promesa de estabilidad en la 4.1.0 en lugar de creértela ahora.
No instales nunca la 2.0.0 ni la 2.1.0: ambas están obsoletas en npm por reventar al importarlas.
¿Cuál es la trampa que me va a morder primero?
La validación recolecta reglas rotas y nunca lanza. Nada impide que un agregado inválido se escape salvo que tu fábrica compruebe isValid por su cuenta. La segunda: el constructor base llama a addValidators() antes de que corra el cuerpo del constructor de tu subclase, así que un validador que lea un campo que asignas ahí lanza en cada construcción.
Edité libs/ddd y la app en marcha no cambió. ¿Por qué?
Sólo las pruebas leen libs/ddd/src — el moduleNameMapper de Jest apunta ahí. tsconfig.json no tiene mapeo de rutas, así que nest build y start:dev resuelven el paquete desde node_modules. Ejecuta npm run build:lib e instala el tarball, o añade un mapeo de rutas, si quieres que la app ejercite tus cambios. Esta separación puede esconder bugs: pruebas en verde contra el fuente local mientras la app en marcha usa otra versión distinta.
¿Este repositorio es la librería o una app de ejemplo?
Las dos cosas, y la librería es lo importante. libs/ddd/ es el paquete publicado; src/ es el ejemplo que lo consume — Pedidos y Productos. Si encuentras algo que describa un módulo Singers, es anterior a la 4.0.0 y está obsoleto; los docs/ se reescribieron contra el código que hay de verdad.
Node >=20.11. Cinco dependencias par, todas obligatorias:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
@nestjs/cqrs ^10.0.0 || ^11.0.0
rxjs ^7.2.0
reflect-metadata ^0.1.13 || ^0.2.0
Que faltara @nestjs/cqrs es lo que hacía que la 2.0.0 reventara al importarla para todo el que no la hubiera instalado por su cuenta.
npm install
npm test # 42 suites, 1111 pruebas, ~12s
npm run test:e2e # 17 pruebas sobre la superficie HTTP real
npm run test:cov:all # ambas, combinadas en un informe — 85 % de líneas
npm run type-check
npm run lintLas dos baterías cubren mitades distintas de la aplicación. npm test cubre el dominio — agregados, value objects, validadores. npm run test:e2e cubre el cableado, conduciendo controladores, handlers y el filtro de excepciones sobre HTTP real.
Informadas por separado, la capa de aplicación marcaba 0 % mientras diecisiete pruebas e2e la estaban recorriendo. test:cov:all combina las dos, y eso lleva la cifra del 64 % al 85 % — un fichero está cubierto si un test lo recorre, y cuál de los dos ejecutores lo hizo es un accidente de cómo repartiste las suites.
Se buscan colaboraciones, y hay trabajo concreto y verificable esperando. Cada punto de abajo se confirmó ejecutándolo.
Buenos primeros issues, más o menos por orden de valor:
- Dale a
Orderun ciclo de vida más rico.Order.startProcessing()existe en el agregado y ningún endpoint llega a él, así queshipydeliverson inalcanzables desde la API — ambos responden409desdeCONFIRMED. - Persiste algo. Los repositorios en memoria son deliberados, pero una segunda implementación contra un almacén real demostraría que el contrato aguanta.
Antes de abrir un PR, CI ejecutará: ESLint, prettier --check, tsc --noEmit contra ambos tsconfig.json y libs/ddd/tsconfig.lib.json, pruebas unitarias con cobertura en Node 18 / 20 / 22, pruebas e2e, la construcción de la librería y npm audit --audit-level=moderate. Hoy pasan todas en local, así que el listón es alcanzable:
npm run lint && npm run type-check && npm run test:cov:allLos commits siguen Conventional Commits. Ojo: el hook de husky está inerte en un clon nuevo — package.json no tiene script prepare — así que de momento nada lo aplica en local. Arreglar eso es en sí mismo un PR bienvenido.
docs.nestjslatam.dev — la guía completa en español, del primer value object a la referencia de API.
| Documento | Cubre |
|---|---|
libs/ddd/README.md |
El paquete publicado, al día en 4.0.0 — esto es lo que muestra npm |
src/orders/README.md |
El módulo de Pedidos en detalle, fiel al controlador real |
docs/order-aggregate-implementation.md |
Recorrido por el diseño del agregado |
docs/VALIDATORS_AND_STATES_IMPLEMENTATION.md |
Validadores y seguimiento de estado |
CHANGELOG.md |
Cada versión, incluidas las dos obsoletas y por qué |
docs/architecture.md |
Las cuatro capas, y por qué las dependencias apuntan hacia dentro |
docs/getting-started.md |
Cómo ejecutarlo, y los tres errores que esta librería hace fáciles |
docs/domain-layer.md |
Agregados, value objects, validadores, eventos de dominio |
docs/application-layer.md |
Casos de uso, handlers, consultas, sagas |
docs/infrastructure-layer.md |
Repositorios, y por qué la persistencia está deliberadamente ausente |
docs/api-reference.md |
Cada endpoint, con los códigos de estado que devuelve de verdad |
Tip
La guía completa del CLI → — cada comando y cada opción, recorridos construyendo un dominio completo desde cero hasta diez ficheros que compilan. Vale la pena leerla aunque nunca instales el CLI: es la explicación más clara del idioma de esta librería que existe, porque cada afirmación se produjo ejecutando la herramienta.
Construido y mantenido por BeyondNet Tech junto a la comunidad NestJS Latam.
- Evolith — gobierno de arquitectura ejecutable: un CLI, un servidor MCP y una API REST que comprueban un repositorio contra reglas Rego/OPA, y que informan de una regla que no pudieron evaluar como un fallo en lugar de dejarla pasar en silencio. La misma idea que
ddd validate, un nivel por encima. - Shell.ddd — la contraparte .NET de esta librería: entidades, raíces de agregado, value objects, eventos de dominio y reglas de negocio para C#.
MIT — ver LICENSE.
Impulsado por BeyondNetCode
Web · GitHub · NestJS Latam