Przejdź do treści
Biblioteka skilli
Programowanie · Budowanie i kod

domain

/domain

Tworzy warstwę domeny w NestJS: encje w czystym TypeScript, obiekty wartości, zdarzenia i interfejsy repozytoriów.

Licencja
MIT
Ostatnia zmiana repozytorium
08.10.2026

Ocena i bezpieczeństwo

95/ 100Klasa: dobry
Format25 / 25
Frontmatter z nazwą i opisem, poprawna nazwa, opis do 1024 znaków, nazwa zgodna z folderem.
Odkrywalność25 / 25
Opis mówi, kiedy użyć skilla, ma sensowną długość i jest w 3. osobie.
Treść30 / 35
Co najmniej 150 słów i do 500 linii, nagłówki, listy kroków, przykłady albo kod.
Higiena15 / 15
Bez zaślepek typu TODO, bez ścieżek Windows, z licencją.
Ryzykobrak sygnałów w skanie
Pliki w paczce
9
Skrypty
0
Komentarze HTML
0
Adresy URL w SKILL.md
0
Przestarzałość

Brak sygnałów. Repozytorium zmienione 08.10.2026, bez odwołań do wycofanych modeli i API.

Licencja

MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.

Co robi i kiedy

Tworzy warstwę domeny w NestJS: encje w czystym TypeScript, obiekty wartości, zdarzenia i interfejsy repozytoriów.

Argumenty

/domain Artifact type or entity name (e.g., "entity Order", "vo OrderStatus", "event OrderCreated")
Oryginalny opis (EN)

Use when creating domain layer artifacts for a NestJS bounded context — entities (pure TypeScript), value objects, domain events, repository interfaces, domain services, validators, or data builders. Covers Hexagonal Architecture + DDD with pure events and configurable dispatcher adapters.

Jak działa

  1. Decision Tree
  2. Entity Pattern
  3. Value Object Variants
  4. Repository Interface
  5. Domain Events
  6. Domain Services
  7. Validators
  8. Data Builders
  9. Domain Errors
  10. Rules and Anti-Patterns
Sekcje z nagłówków SKILL.md, w oryginale · 588 linii, 2 133 słów
Podgląd SKILL.md
---
name: domain
description: Use when creating domain layer artifacts for a NestJS bounded context — entities (pure TypeScript), value objects, domain events, repository interfaces, domain services, validators, or data builders. Covers Hexagonal Architecture + DDD with pure events and configurable dispatcher adapters.
argument-hint: Artifact type or entity name (e.g., "entity Order", "vo OrderStatus", "event OrderCreated")
---

# Domain Layer

The domain layer contains the pure business logic of a bounded context. It has zero framework dependencies. No `@Injectable`, no `PrismaService`, no HTTP concerns.

---

## Decision Tree

What are you creating?

```
Domain artifact needed?
│
├── An entity (aggregate root with lifecycle methods)
│   └── Go to: Entity Pattern
│
├── A simple value (email, amount, name, phone)
│   └── Go to: VO — Scalar
│
├── A grouped value (address, bank details, money)
│   └── Go to: VO — Composed
│
├── A fixed set of values (payment method, channel type)
│   └── Go to: VO — Enum
│
├── A status with allowed transitions (order status, withdrawal status)
│   └── Go to: VO — State Machine
│
├── The persistence contract for the entity
│   └── Go to: Repository Interface
│
├── Something that happened in the domain
│   └── Go to: Domain Events
│
├── Logic that belongs to the domain but not to one entity
│   └── Go to: Domain Services
│
├── A domain-specific error with context
│   └── Go to: Domain Errors
│
├── Entity invariant enforcement (field-level rules)
│   └── Go to: Validators
│
└── Fake data for tests
    └── Go to: Data Builders
```

---

## Entity Pattern

Full reference: `references/entity-patterns.md`

An entity is identified by its `UniqueEntityID`, not by its value. It extends the pure TypeScript `Entity` base, which queues events for the handler to publish after persistence.

**Two factory methods are mandatory:**

- `static create(props, id?)` — for new instances; calls `this.apply(new XCreatedEvent(...))`
- `static restore(props, id)` — for DB hydration; emits no events

**Event lifecycle** (every entity follows this):
```
entity.apply(event)            // 1. records the event in memory
await repo.save(entity)        // 2. persists
await transactionCommit       // 3. wait for the actual transaction commit, if present
await events.from(entity).publish() // 4. handler publishes through its adapter
```

**Condensed template:**

```typescript
// domain/entities/<name>.entity.ts
import { Entity } from '@/shared/base-classes/entity';
import { <Name>CreatedEvent } from '../events/<name>-created.event';

export interface <Name>Props {
  tenantId: string;
  // ... business fields
  createdAt: Date;
  updatedAt?: Date;
}

export class <Name>Entity extends Entity<<Name>Props> {
  private constructor(props: <Name>Props, id?: string) {
    super(props, id);
  }

  static create(
    input: Omit<<Name>Props, 'createdAt' | 'updatedAt'>,
    id?: string,
  ): <Name>Entity {
    const entity = new <Name>Entity({ ...input, createdAt: new Date() }, id);
    entity.apply(new <Name>CreatedEvent(entity.id));
    return entity;
  }

  static restore(props: <Name>Props, id: string): <Name>Entity {
    return new <Name>Entity(props, id);
  }

  // Mutating method
  update(name: string): void {
    this.props.name = name;
    this.touch();
    this.apply(new <Name>UpdatedEvent(this.id, name));
  }

  get tenantId(): string { return this.props.tenantId; }
  // ... other getters

  get createdAt(): Date { return this.props.createdAt; }
  get updatedAt(): Date | undefined { return this.props.updatedAt; }
}
```

See `references/entity-patterns.md` for: child entities, `validate()` integration, `toJSON()` override, and the full test template.

---

## Value Object Variants

Full reference: `references/value-object-patterns.md`

All VOs extend `ValueObject<T>` from `@/shared/base-classes/value-object`. Rules:
- Constructor is `private` — only static factories create instances
- `validate()` throws on invariant violations; it is called automatically in the constructor
- Never import `@nestjs/*` or `class-validator` in a VO

### Scalar VO (single value)

```typescript
// domain/value-objects/email.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';
import { InvalidArgumentError } from '@/shared/domain-errors/errors';

export class EmailVO extends ValueObject<string> {
  private constructor(value: string) { super(value); }

  protected validate(): void {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this._value)) {
      throw new InvalidArgumentError(`Invalid email: "${this._value}"`);
    }
  }

  static create(email: string): EmailVO {
    return new EmailVO(email.toLowerCase().trim());
  }

  get domain(): string { return this._value.split('@')[1]; }
}
```

### Composed VO (multiple props)

```typescript
// domain/value-objects/address.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';
import { InvalidArgumentError } from '@/shared/domain/errors';

interface AddressProps { street: string; city: string; country: string; }

export class AddressVO extends ValueObject<AddressProps> {
  private constructor(props: AddressProps) { super(props); }

  protected validate(): void {
    if (!this._value.street) throw new InvalidArgumentError('Street is required');
    if (!this._value.city)   throw new InvalidArgumentError('City is required');
    if (!this._value.country) throw new InvalidArgumentError('Country is required');
  }

  static create(props: AddressProps): AddressVO {
    return new AddressVO(props);
  }

  get street(): string  { return this._value.street; }
  get city(): string    { return this._value.city; }
  get country(): string { return this._value.country; }
}
```

### Enum VO (fixed set of named values)

```typescript
// domain/value-objects/payment-method.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';

export enum PaymentMethodEnum { CREDIT = 'CREDIT', DEBIT = 'DEBIT', PIX = 'PIX' }

export class PaymentMethodVO extends ValueObject<PaymentMethodEnum> {
  private constructor(value: PaymentMethodEnum) { super(value); }

  protected validate(): void {
    if (!Object.values(PaymentMethodEnum).includes(this._value)) {
      throw new InvalidArgumentError(`Unknown payment method: ${this._value}`);
    }
  }

  static credit(): PaymentMethodVO { return new PaymentMethodVO(PaymentMethodEnum.CREDIT); }
  static debit():  PaymentMethodVO { return new PaymentMethodVO(PaymentMethodEnum.DEBIT); }
  static pix():    PaymentMethodVO { return new PaymentMethodVO(PaymentMethodEnum.PIX); }

  static from(value: string): PaymentMethodVO {
    return new PaymentMethodVO(value as PaymentMethodEnum);
  }

  isCredit(): boolean { return this._value === PaymentMethodEnum.CREDIT; }
  isPix(): boolean    { return this._value === PaymentMethodEnum.PIX; }
}
```

### State Machine VO (controlled transitions)

```typescript
// domain/value-objects/order-status.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';

export enum OrderStatusEnum {
  PENDING = 'PENDING', CONFIRMED = 'CONFIRMED', SHIPPED = 'SHIPPED', COMPLETED = 'COMPLETED',
}

const ALLOWED_TRANSITIONS: Record<OrderStatusEnum, OrderStatusEnum[]> = {
  [OrderStatusEnum.PENDING]:    [OrderStatusEnum.CONFIRMED],
  [OrderStatusEnum.CONFIRMED]:  [OrderStatusEnum.SHIPPED],
  [OrderStatusEnum.SHIPPED]:    [OrderStatusEnum.COMPLETED],
  [OrderStatusEnum.COMPLETED]:  [],
};

export class OrderStatusVO extends ValueObject<OrderStatusEnum> {
  private constructor(value: OrderStatusEnum) { super(value); }

  protected validate(): void {
    if (!Object.values(OrderStatusEnum).includes(this._value)) {
      throw new InvalidArgumentError(`Invalid order status: ${this._value}`);
    }
  }

  static pending():   OrderStatusVO { return new OrderStatusVO(OrderStatusEnum.PENDING); }
  static confirmed(): OrderStatusVO { return new OrderStatusVO(OrderStatusEnum.CONFIRMED); }

  static from(value: string): OrderStatusVO {
    return new OrderStatusVO(value as OrderStatusEnum);
  }

  canTransitionTo(target: OrderStatusEnum): boolean {
    return ALLOWED_TRANSITIONS[this._value].includes(target);
  }

  transitionTo(target: OrderStatusEnum): OrderStatusVO {
    if (!this.canTransitionTo(target)) {
      throw new InvalidArgumentError(
        `Cannot transition from ${this._value} to ${target}`,
      );
    }
    return new OrderStatusVO(target);
  }

  isPending():   boolean { return this._value === OrderStatusEnum.PENDING; }
  isCompleted(): boolean { return this._value === OrderStatusEnum.COMPLETED; }
}
```

See `references/value-object-patterns.md` for full VO test templates and edge-case guidance.

---

## Repository Interface

Full reference: `references/repository-interface.md`

The repository interface is the domain's **port** for persistence. It lives in `domain/repositories/`. The implementation lives in `infrastructure/`. No `@Injectable` or Prisma imports here.

```typescript
// domain/repositories/<name>.repository.ts
import type { <Name>Entity } from '../entities/<name>.entity';
import {
  SearchParams as DefaultSearchParams,
  SearchResult as DefaultSearchResult,
  SearchableRepositoryInterface,
} from '@/shared/repository-contracts/searchable-repository-contracts';

export namespace <Name>Repository {
  export type Filter = string;
  export class SearchParams extends DefaultSearchParams<Filter> {}
  export class SearchResult extends DefaultSearchResult<<Name>Entity, Filter> {}

  export interface Repository
    extends SearchableRepositoryInterface<
      <Name>Entity, Filter, SearchParams, SearchResult
    > {
    findById(id: string): Promise<<Name>Entity | null>;
    findByTenant(tenantId: string): Promise<<Name>Entity[]>;
    save(entity: <Name>Entity): Promise<void>;
    delete(id: string): Promise<void>;
  }
}

export const <NAME>_REPOSITORY = Symbol('<Name>Repository');
```

See `references/repository-interface.md` for: extended SearchParams with custom filters, domain-specific query methods, and the in-memory test double pattern.

---

## Domain Events

Full reference: `references/domain-event-patterns.md`

Domain events capture **what happened** in the domain. They are pure TypeScript and carry stable `eventName`, `aggregateId`, `eventId` and `occurredOn` metadata.

**Naming:** `<Entity><PastTenseVerb>Event` (e.g., `OrderPlacedEvent`, `CustomerActivatedEvent`)

```typescript
// domain/events/<name>-created.event.ts
import { DomainEvent } from '@/shared/base-classes/domain-event';

export class <Name>CreatedEvent extends DomainEvent {
  constructor(
    public readonly <name>Id: string,
    public readonly tenantId: string,
    // include all data handlers will need
  ) {
    super('<context>.<name>.created', <name>Id);
  }
}
```

Rules:
- Events are applied via `entity.apply(event)` inside entity methods
- The handler calls `await events.from(entity).publish()` after the actual transaction commit to publish
- Event handlers use `@EventsHandler` + `IEventHandler` — never `@OnEvent` for new code
- Include enough payload so handlers do not need to re-fetch the entity

See `references/domain-event-patterns.md` for: full event catalog pattern, testing uncommitted events, and handler template.

---

## Domain Services

Full reference: `references/domain-service-patterns.md`

Use a domain service when a domain operation involves multiple entities or rules that do not belong to a single entity. Domain services are **pure classes** — no decorators, no DI, no I/O.

```typescript
// domain/services/<name>.service.ts
export class FeeCalculationService {
  static calculate(amount: number, feePercentage: number): number {
    if (amount <= 0) throw new InvalidArgumentError('Amount must be positive');
    return Math.round(amount * (feePercentage / 100));
  }
}
```

**When to use:**
- Logic spans two or more aggregates
- A calculation or rule is shared by multiple entity methods
- The operation is stateless and has no side effects

**When NOT to use:**
- Logic belongs clearly to a single entity → put it in the entity
- Logic requires I/O (DB, queue) → use an application service in the `application/` layer

See `references/domain-service-patterns.md` for: stateful services, integration examples, and test templates.

---

## Validators

Full reference: `references/validator-patterns.md`

Validators enforce entity invariants (field-level rules) using `class-validator` decorators on a `Rules` class, executed via `ClassValidatorFields`. This is for **domain validation only** — not for HTTP input validation.

```typescript
// domain/validators/<name>.validator.ts
import { IsString, IsNotEmpty, IsUUID, IsOptional, MaxLength } from 'class-validator';
import { ClassValidatorFields } from '@/shared/base-classes/class-validator-fields';
import type { <Name>Props } from '../entities/<name>.entity';

class <Name>Rules {
  @IsUUID() @IsNotEmpty() tenantId: string;
  @IsString() @IsNotEmpty() @MaxLength(255) name: string;
  @IsOptional() @IsString() description?: string;

  constructor(props: <Name>Props) { Object.assign(this, props); }
}

class <Name>Validator extends ClassValidatorFields<<Name>Rules> {
  validate(data: <Name>Props): boolean {
    return super.validate(new <Name>Rules(data));
  }
}

export class <Name>ValidatorFactory {
  static create(): <Name>Validator { return new <Name>Validator(); }
}
```

Usage in entity:

```typescript
static validate(props: <Name>Props): void {
  const validator = <Name>ValidatorFactory.create();
  if (!validator.validate(props)) {
    throw new EntityValidationError(validator.errors);
  }
}
```

See `references/validator-patterns.md` for: the `ClassValidatorFields` base, nested validation, and full test template.

---

## Data Builders

Full reference: `references/data-builder-patterns.md`

Data builders provide deterministic fake data for unit tests. They live in `domain/testing/helpers/`.

```typescript
// domain/testing/helpers/<name>.data-builder.ts
import { faker } from '@faker-js/faker';
import type { <Name>Props } from '../../entities/<name>.entity';

export function <Name>DataBuilder(overrides?: Partial<<Name>Props>): <Name>Props {
  return {
    tenantId: overrides?.tenantId ?? faker.string.uuid(),
    name:     overrides?.name     ?? faker.commerce.productName(),
    createdAt: overrides?.createdAt ?? new Date(),
    updatedAt: overrides?.updatedAt,
    ...overrides,
  };
}
```

See `references/data-builder-patterns.md` for: composed builders, state-specific variants (Active, Pending, Rejected), and how to wire builders into test `describe` blocks.

---

## Domain Errors

Specialized errors that extend the shared error hierarchy (`DomainError`, `NotFoundError`, `ConflictError`, etc.) with domain-specific context.

Live in `<bc>/domain/errors/`. Name convention: `<Entity><Condition>Error`.

```typescript
// domain/errors/order-not-found.error.ts
import { NotFoundError } from '@/shared/domain/errors';

export class OrderNotFoundError extends NotFoundError {
  constructor(public readonly orderId: string) {
    super(`Order ${orderId} not found`);
  }
}

// domain/errors/invalid-order-status-transition.error.ts
import { DomainError } from '@/shared/domain/errors';

export class InvalidOrderStatusTransitionError extends DomainError {
  constructor(
    public readonly currentStatus: string,
    public readonly targetStatus: string,
  ) {
    super(`Cannot transition from ${currentStatus} to ${targetStatus}`);
  }
}
```

**When to specialize:** When the error carries domain context (IDs, amounts, statuses) that helps debugging. Don't specialize if the generic message is sufficient.

See `references/domain-error-patterns.md` for: full error catalog, naming conventions, error filter mapping, testing patterns.

---

## Rules and Anti-Patterns

### What belongs in the domain layer

- Entity classes, VO classes, domain events, repository interfaces, domain service classes, validators
- Types and interfaces that describe domain concepts

### What does NOT belong here

| Anti-pattern | Correct approach |
|---|---|
| `import { Injectable } from '@nestjs/common'` | Only allowed in `infrastructure/` and `application/services/` |
| `import { PrismaService }` | Only in Prisma repository implementations |
| `import { IsEmail } from 'class-validator'` | Only in validators (not in VOs) |
| `new PrismaClient()` inside an entity | Repositories handle persistence |
| Calling another aggregate's repository | Use a domain service or application service |
| Throwing HTTP exceptions (`NotFoundException`) | Throw domain errors; HTTP mapping is in `infrastructure/` |
| `entity.addDomainEvent()` / `entity.pullDomainEvents()` | Use pure `entity.apply(event)` and handler dispatch |

### Event invariants

- `create()` ALWAYS calls `this.apply(new XCreatedEvent(...))`
- `restore()` NEVER emits events
- Mutating methods call `this.touch()` then `this.apply(new XUpdatedEvent(...))`
- The repository saves the entity; the CQRS handler publishes events

### VO invariants

- VOs are immutable — mutating methods return a NEW VO instance
- State Machine VOs always validate transition legality in `transitionTo()`
- Enum VOs provide named static factories (`static pending()`) — callers never pass raw strings

---

## Testing

### Entity spec skeleton

```typescript
// domain/entities/__tests__/unit/<name>.entity.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { <Name>Entity } from '../../<name>.entity';
import { <Name>DataBuilder } from '../../../testing/helpers/<name>.data-builder';
import { <Name>CreatedEvent } from '../../../events/<name>-created.event';

describe('<Name>Entity', () => {
  describe('create()', () => {
    it('should create entity with valid props', () => {
      const props = <Name>DataBuilder();
      const entity = <Name>Entity.create(props);

      expect(entity.id).toBeDefined();
      expect(entity.name).toBe(props.name);
      expect(entity.createdAt).toBeInstanceOf(Date);
    });

    it('should apply <Name>CreatedEvent', () => {
      const entity = <Name>Entity.create(<Name>DataBuilder());
      const events = entity.getUncommittedEvents();

      expect(events).toHaveLength(1);
      expect(events[0]).toBeInstanceOf(<Name>CreatedEvent);
    });
  });

  describe('restore()', () => {
    it('should not apply any events', () => {
      const props = <Name>DataBuilder();
      const entity = <Name>Entity.restore(props, faker.string.uuid());

      expect(entity.getUncommittedEvents()).toHaveLength(0);
    });
  });

  describe('update()', () => {
    it('should update field and touch updatedAt', () => {
      const entity = <Name>Entity.create(<Name>DataBuilder());
      entity.getUncommittedEvents().forEach((event) => entity.acknowledgeEvent(event)); // discard creation events in this fixture only

      entity.update('new-name');

      expect(entity.name).toBe('new-name');
      expect(entity.updatedAt).toBeInstanceOf(Date);
    });
  });
});
```

### VO spec skeleton

```typescript
// domain/value-objects/__tests__/<name>.vo.spec.ts
import { describe, it, expect } from 'vitest';
import { EmailVO } from '../email.vo';

describe('EmailVO', () => {
  it('should create from valid email', () => {
    const vo = EmailVO.create('User@Example.COM');
    expect(vo.value).toBe('user@example.com');
  });

  it('should throw on invalid email', () => {
    expect(() => EmailVO.create('not-an-email')).toThrow();
  });

  it('should be equal when value matches', () => {
    expect(EmailVO.create('a@b.com').equals(EmailVO.create('a@b.com'))).toBe(true);
  });
});
```

## Event dispatcher reference

See [Fluent event dispatcher](../application/references/event-dispatcher.md) for typed named events, instance preservation, container providers, transaction ordering, queue acknowledgement, adapters and migration compatibility.
Pliki w paczce (9)
  • SKILL.md 20 KB
  • references/data-builder-patterns.md 8 KB
  • references/domain-error-patterns.md 8 KB
  • references/domain-event-patterns.md 11 KB
  • references/domain-service-patterns.md 9 KB
  • references/entity-patterns.md 12 KB
  • references/repository-interface.md 8 KB
  • references/validator-patterns.md 10 KB
  • references/value-object-patterns.md 17 KB

Wersje

RepozytoriumWynikLicencja
Ta strona · softtor/nestjs-hexagonal95MIT
boshu2/agentops100Apache-2.0
coreyhaines31/makerskills100MIT
davidsouther/ailly100MIT
controlplane-com/ai-plugin100MIT

Migawka bazy z 28.10.2026 · wersja paczki f9ba95e2946d (skrót SHA-256 całej paczki)

Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.

Podobne skille

  • 100domain-model
    Tworzy lub recenzuje model domeny: bounded contexts, encje, value objects, agregaty, zdarzenia domenowe i relacje.
  • 100clean-architecture
    Dostarcza wzorce Clean Architecture, DDD i architektury heksagonalnej dla aplikacji NestJS i TypeScript, od warstwy domeny po porty i adaptery.
  • 100nest-ddd-conventions
    Wprowadza konwencje DDD w NestJS: warstwę domeny, porty i adaptery, konteksty ograniczone jako moduły oraz czystość domeny w TypeScript.
  • 95domain-driven-design
    Opisuje wzorce Domain-Driven Design w TypeScript: język wszechobecny, value objects, encje, agregaty, zdarzenia domenowe, serwisy domenowe i bounded contexts.
  • 100ddd
    Wspiera tworzenie systemów w Domain-Driven Design: projektowanie nowych systemów, refaktoryzacja ku DDD, szkielety kodu (encje, agregaty, repozytoria, zdarzenia), Event Storming, mapy kontekstów i przeglądy kodu.
  • 100domain-driven-design
    Modeluje oprogramowanie wokół domeny biznesowej przez bounded contexts, agregaty i język wszechobecny, w tym encje a value objects, zdarzenia domenowe, mapowanie kontekstów i anti-corruption layer.

Uwagi i zgłoszenia

Wiadomość trafi do Bartosza Sokolińskiego. Adres e-mail jest potrzebny tylko do odpowiedzi. Szczegóły w polityce prywatności.