Vamos a construir un Product completo: un value object con reglas, un agregado con una invariante que sólo él puede juzgar, y el endpoint que devuelve 422 cuando algo no cuadra. Todo el código está sacado del repositorio de ejemplo, donde corre en CI.
Paso 1 · Instalar
npm i @nestjslatam/ddd-lib@4.0.0 @nestjs/cqrs
Sin ^. La API se ha movido entre versiones mayores y el compilador no detecta la mayoría de los cambios; clavar la versión es lo que te deja actualizar cuando tú decidas.
Paso 2 · Un value object con reglas
import { StringValueObject, AbstractRuleValidator, BrokenRulesException }
from \'@nestjslatam/ddd-lib\';
class NameLengthValidator extends AbstractRuleValidator<Name> {
public addRules(): void {
const value = this.subject.getValue();
// OJO: la condición es verdadera cuando la regla está ROTA.
if (value.length < 3) this.addBrokenRule(\'value\', \'Mínimo 3 caracteres\');
if (value.length > 100) this.addBrokenRule(\'value\', \'Máximo 100 caracteres\');
}
}
export class Name extends StringValueObject {
static create(value: string): Name {
const name = new Name(value);
if (!name.isValid) { // ← imprescindible
throw new BrokenRulesException(\'Name\', name.brokenRules.getBrokenRules());
}
return name;
}
override addValidators(): void {
super.addValidators(); // ← imprescindible
this.validatorRules.add(new NameLengthValidator(this));
}
}
Las dos líneas marcadas son el 80 % de los fallos que verás al empezar, y ninguna de las dos produce un error si falta:
- Sin
if (!name.isValid)→create()devuelve objetos inválidos, en silencio. - Sin
super.addValidators()→ los validadores de la clase base desaparecen, y\'\'pasa a ser un nombre válido.
Paso 3 · El agregado
export class Product extends DddAggregateRoot<Product, IProductProps> {
private constructor(props: IProductProps, id?: IdValueObject) {
super(props, { id }); // el id va en la BOLSA DE OPCIONES
this.trackingState.markAsNew();
}
static create(name: Name, description: Description, price: Price): Product {
const product = new Product({ name, description, price, status: ProductStatus.ACTIVE });
if (!product.isValid) {
throw new BrokenRulesException(\'Product\', product.brokenRules.getBrokenRules());
}
product.apply(new ProductCreatedEvent(product.id.getValue()));
return product;
}
override addValidators(): void {
super.addValidators();
this.validatorRules.add(new ProductBusinessRulesValidator(this));
}
}
Tres detalles que cuestan una tarde si los descubres solo: dos argumentos de tipo (<Product, IProductProps>), el id va dentro de { id }, y markAsNew() lo llamas tú — el seguimiento de estado no adivina.
Paso 4 · Una regla que sólo el agregado puede ver
export class ProductBusinessRulesValidator extends AbstractRuleValidator<Product> {
public addRules(): void {
const { name, description } = this.subject.props;
if (description.getValue().length <= name.getValue().length) {
this.addBrokenRule(\'props.description\', \'La descripción debe ser más larga que el nombre\');
}
}
}
Aquí es donde el patrón empieza a pagar. name es válido. description es válida. La combinación no lo es, y ningún value object puede saberlo porque ninguno ve al otro. La raíz de agregado es el único sitio del sistema desde donde esa frase se puede escribir.
Paso 5 · El handler
@CommandHandler(CreateProductCommand)
export class CreateProductCommandHandler implements ICommandHandler<CreateProductCommand, string> {
constructor(
private readonly publisher: EventPublisher,
private readonly repository: ProductRepository,
) {}
async execute(command: CreateProductCommand): Promise<string> {
const product = Product.create(
Name.create(command.name),
Description.create(command.description),
Price.create(command.price),
);
await this.repository.save(product); // 1. persistir
this.publisher.mergeObjectContext(product).commit(); // 2. despachar
return product.id.getValue();
}
}
El orden importa: publicar antes de guardar abre una ventana en la que un manejador lee un producto que todavía no existe. Y sin .commit() el comando triunfa, devuelve su respuesta, y todos tus @EventsHandler se saltan en silencio.
Paso 6 · Comprobar que funciona
npx ddd validate
Audita las cuatro reglas del idioma contra tu código y tu versión instalada. Es un comando determinista, sin modelo, y se puede poner en CI tal cual.
Para seguir
La versión larga, con el ciclo de vida de un pedido y las entidades hijas, está en Tu primer agregado. Y la guía del CLI construye un dominio entero comando a comando.
