@nestjslatam/ddd-lib
v4.0.0
Published
A comprehensive Domain-Driven Design library for NestJS - Build scalable, maintainable enterprise applications
Maintainers
Readme
@nestjslatam/ddd-lib
Domain-Driven Design building blocks for NestJS: aggregate roots that validate themselves, value objects that reject invalid input, and the broken-rules and state-tracking machinery behind both.
npm install @nestjslatam/ddd-lib[!WARNING] Pre-1.0 in spirit. The public API is not stable and this is not recommended for production. Pin an exact version.
2.0.0and2.1.0are deprecated on npm and should not be used.2.0.0crashes on import wherever@nestjs/cqrsis not already installed;2.1.0breaks every CommonJS consumer, Jest included, through an ESM-onlyuuid. Current is3.0.0, which unifiesisValidon a getter -- see below.
A value object
import { NumberValueObject, NumberPositiveValidator } from '@nestjslatam/ddd-lib';
export class Price extends NumberValueObject {
static create(value: number): Price {
const price = new Price(value);
// Validation collects broken rules; it never throws. A factory that does
// not check is a factory that returns invalid objects.
if (!price.isValid) {
throw new Error(price.brokenRules.getBrokenRulesAsString());
}
return price;
}
override addValidators(): void {
super.addValidators();
this.validatorRules.add(new NumberPositiveValidator(this));
}
}
Price.create(-1);
// Error: Property: value, Message: value must be a positive number (greater than zero)An aggregate root
import {
AbstractRuleValidator,
DddAggregateRoot,
IdValueObject,
} from '@nestjslatam/ddd-lib';
interface IOrderProps {
total: Price;
}
class OrderApprovalValidator extends AbstractRuleValidator<Order> {
addRules(): void {
if (this.subject.props.total.getValue() > 10_000) {
this.addBrokenRule('total', 'Orders over 10,000 need manual approval');
}
}
}
export class Order extends DddAggregateRoot<Order, IOrderProps> {
private constructor(props: IOrderProps, id?: IdValueObject) {
super(props, { id });
this.trackingState.markAsNew();
}
static create(total: Price): Order {
const order = new Order({ total });
// Note the parentheses. See "Two shapes of isValid" below.
if (!order.isValid()) {
throw new Error(order.brokenRules.getBrokenRulesAsString());
}
return order;
}
addValidators(): void {
this.validators.add(new OrderApprovalValidator(this));
}
}isValid is a getter
if (!order.isValid) { ... } // aggregate
if (!price.isValid) { ... } // value objectBoth bases expose it the same way. This changed in 3.0.0 — DddAggregateRoot previously declared it as a method, and the mismatch made a silent defect easy to write: if (!order.isValid) tested a Function, always truthy, so the guard never fired. TypeScript did not flag it, and since validation only collects broken rules and never throws, nothing else did either. Three such guards shipped in this repository's own sample.
Upgrading from 2.x, TypeScript points at every call site: TS6234: This expression is not callable because it is a 'get' accessor. For a mechanical pass, npx ddd validate reports them all.
What you get
Aggregates. DddAggregateRoot<TEntity, TProps, TState extends object = object> — self-validating, with equality, identity, serialization and a state machine. Its constructor takes (props, options?), where the options carry an id, replacement managers, and skipInitialValidation. Override addValidators(manager) to register rules, guard() for construction-time checks, and defineValidTransitions(map) plus canTransitionTo(from, to) for a lifecycle.
Value objects. DddValueObject and three specializations you subclass: StringValueObject, NumberValueObject, IdValueObject. All have protected constructors — reach them through static factories.
Validation. AbstractRuleValidator<TSubject> for a single rule set, implementing addRules() and calling addBrokenRule(property, message). AbstractValidator with EntityValidator and ValueObjectValidator for the entity-wide pass. ValidatorRuleManager holds them; BrokenRulesManager collects the results and answers getBrokenRules(), getBrokenRulesAsString(), hasErrors() and clear().
Events. DomainEvent, also exported as AbstractDomainEvent — they are the same class. EventMetadataBuilder.create(id, type, version).withCorrelationId(...).build() builds the metadata. Events serialize to eventId, eventType, eventVersion, occurredOn, metadata and data.
State tracking. TrackingStateManager exposes isNew, isDirty, isDeleted and isSelfDeleted, with a markAs* method for each plus markAsClean. StateTransitionManager and TrackingStateTransition handle lifecycle transitions.
Exceptions. DomainException and five specializations: ArgumentNullException, InvalidFormatException, InvalidOperationException, InvalidStateTransitionException, NoTransitionsDefinedException.
Forty-four symbols in all. npx ddd list prints the full inventory grouped by family, read from the copy installed in your project.
Requirements
- Node 20.11 or later.
- Peer dependencies:
@nestjs/common,@nestjs/coreand@nestjs/cqrs(^10or^11), plusreflect-metadataandrxjs. All are declared; npm 7 and later install missing ones for you. - One bundled dependency:
uuid@^11, used byIdValueObjectand byDomainEventto generate everyeventId. It is not optional if you use either.
Known limitations
Honest list, all verified against the current release:
aggregate.versionisundefined. The private setter has no caller.- An aggregate does not clear stale broken rules on re-validation. Fix the data, call
validate()again, and the old rule is still there — you mustbrokenRules.clear()first. Value objects do clear; aggregates do not. DddService.explore()is empty, and so isDddModule.onApplicationBootstrap().- The repository interfaces ship no implementation.
find,findById,insert,insertBatch,updateanddeleteare contracts for you to satisfy. propsCopyis frozen one level deep. The wrapper is frozen; the nestedpropsobject is not.
The ecosystem
| Package | What it is |
|---|---|
| @nestjslatam/ddd-lib | These building blocks — you are here |
| @nestjslatam/ddd-cli | Inventory the stereotypes, scaffold them, subclass them, audit your code against this library's idiom. Runs as an MCP server so an AI agent can drive it |
| @nestjslatam/ddd-valueobjects | Ready-made value objects: email, phone number, money, date range, document id |
| @nestjslatam/ddd-es-lib | Event sourcing: event store, snapshots, upcasting, sagas, materialised views |
Documentation
The repository carries a working sample — Orders and Products — under src/, and it is the best reference for how these pieces fit together. order-aggregate-implementation.md walks through it.
[!NOTE] Six older documents in
docs/describe asingersmodule that no longer exists, and one of them documents a TypeORM setup removed in 2.1.0. They are useful for the general shape of a DDD application, not as a guide to the code that ships. The repository README says which is which.
Links
- Repository · Changelog · Issues
- NestJS Latam — the community behind these packages
License
MIT. Author: Alberto Arroyo Raygada.
