From nestjs-hexagonal
Use when creating application layer artifacts for a NestJS bounded context — use cases, CQRS command/query handlers, DTOs, ports (cross-module interfaces), application services, or read model projections. Supports three patterns: plain UseCase (A), CQRS native (B), and Handler-as-Orchestrator (C).
How this skill is triggered — by the user, by Claude, or both
Slash command
/nestjs-hexagonal:applicationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **Note:** Examples use `companyId` as the multi-tenant identifier. Replace with your project's term (e.g., `organizationId`, `tenantId`).
Note: Examples use
companyIdas the multi-tenant identifier. Replace with your project's term (e.g.,organizationId,tenantId).
This skill covers everything inside a bounded context's application/ directory:
use cases, CQRS handlers, DTOs, ports, application services, and Redis read models.
Work through this flowchart before writing a single line of code.
Does the module already use CQRS (CommandBus / QueryBus)?
├── No → Pattern A (plain UseCase + TOKEN)
└── Yes →
Is this a simple read (findById) with no RBAC or transformation?
├── Yes → Skip use case — inject repository directly in controller
└── No →
Does orchestration span multiple services (QueryBus calls,
multiple ports, complex enrichment)?
├── Yes → Pattern C (Handler as Orchestrator)
└── No → Pattern B (CQRS Command/Query)
Need read model / projections?
└── CQRS R/W separation: write side → Pattern B/C, read side → Redis query handler
(see references/read-model-patterns.md)
Reusable logic between 2+ handlers?
└── Extract into Application Service (@Injectable, in application/services/)
Shared domain logic (no I/O, no framework)?
└── Extract into Domain Service (plain class, in domain/services/)
Use when the context uses plain use cases without CQRS (e.g., customers, contacts).
application/
├── dtos/
│ └── create-<context>.dto.ts
└── usecases/
├── create-<context>.usecase.ts
└── __tests__/
└── create-<context>.usecase.spec.ts
// application/dtos/create-<context>.dto.ts
export namespace Create<Context>Dto {
export interface Input {
companyId: string;
name: string;
// add domain-specific fields
}
export interface Output {
id: string;
companyId: string;
name: string;
createdAt: Date;
}
}
No @Injectable, no NestJS imports. Pure TypeScript class.
// application/usecases/create-<context>.usecase.ts
import type { <Context>Repository } from '../../domain/repositories/<context>.repository';
import type { Create<Context>Dto } from '../dtos/create-<context>.dto';
import { <Context>Entity } from '../../domain/entities/<context>.entity';
import { OutputMapper } from '../dtos/<context>-output.dto';
export const CREATE_<CONTEXT>_USE_CASE_TOKEN = Symbol('Create<Context>UseCase');
export namespace Create<Context>UseCase {
export type Input = Create<Context>Dto.Input;
export type Output = Create<Context>Dto.Output;
export class UseCase {
constructor(
private readonly repository: <Context>Repository.Repository,
) {}
async execute(input: Input): Promise<Output> {
const entity = <Context>Entity.create({
companyId: input.companyId,
name: input.name,
});
await this.repository.insert(entity);
return OutputMapper.toOutput(entity);
}
}
}
// infrastructure/<context>.module.ts
import {
CREATE_<CONTEXT>_USE_CASE_TOKEN,
Create<Context>UseCase,
} from '../application/usecases/create-<context>.usecase';
import { <CONTEXT>_REPOSITORY_TOKEN } from '../domain/repositories/<context>.repository';
@Module({
providers: [
Prisma<Context>Repository,
{
provide: <CONTEXT>_REPOSITORY_TOKEN,
useExisting: Prisma<Context>Repository,
},
{
provide: CREATE_<CONTEXT>_USE_CASE_TOKEN,
useFactory: (repo: <Context>Repository.Repository) =>
new Create<Context>UseCase.UseCase(repo),
inject: [<CONTEXT>_REPOSITORY_TOKEN],
},
],
exports: [<CONTEXT>_REPOSITORY_TOKEN], // NEVER export use cases
})
export class <Context>Module {}
constructor(
@Inject(CREATE_<CONTEXT>_USE_CASE_TOKEN)
private readonly createUseCase: Create<Context>UseCase.UseCase,
) {}
Full template: references/usecase-plain.md
Use when the module already integrates CqrsModule. Handlers are self-registering via @CommandHandler / @QueryHandler — no token needed.
application/
├── dtos/
│ ├── create-<context>.dto.ts
│ └── get-<context>.dto.ts
├── commands/
│ ├── create-<context>.command.ts
│ └── create-<context>.handler.ts
└── queries/
├── get-<context>.query.ts
└── get-<context>.handler.ts
// application/commands/create-<context>.command.ts
import { Command } from '@nestjs/cqrs';
import type { Create<Context>Dto } from '../dtos/create-<context>.dto';
export class Create<Context>Command extends Command<Create<Context>Dto.Output> {
constructor(
public readonly companyId: string,
public readonly name: string,
) {
super();
}
}
// application/commands/create-<context>.handler.ts
import { Inject } from '@nestjs/common';
import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs';
import type { <Context>Repository } from '../../domain/repositories/<context>.repository';
import { <CONTEXT>_REPOSITORY_TOKEN } from '../../domain/repositories/<context>.repository';
import { <Context>Entity } from '../../domain/entities/<context>.entity';
import type { Create<Context>Dto } from '../dtos/create-<context>.dto';
import { Create<Context>Command } from './create-<context>.command';
@CommandHandler(Create<Context>Command)
export class Create<Context>Handler
implements ICommandHandler<Create<Context>Command, Create<Context>Dto.Output>
{
constructor(
@Inject(<CONTEXT>_REPOSITORY_TOKEN)
private readonly repository: <Context>Repository.Repository,
private readonly publisher: EventPublisher,
) {}
async execute(command: Create<Context>Command): Promise<Create<Context>Dto.Output> {
// Create entity (entity.apply(event) happens inside Entity.create)
const entity = <Context>Entity.create({
companyId: command.companyId,
name: command.name,
});
await this.repository.insert(entity);
// mergeObjectContext and commit happen in the HANDLER — never in the use case
this.publisher.mergeObjectContext(entity);
entity.commit(); // dispatches domain events via EventBus
return { id: entity.id };
}
}
Key rules for command handlers:
void or { id: string } — never the full aggregate.EventPublisher is injected ONLY in the handler, never in use cases.mergeObjectContext(entity) + entity.commit() happen in the handler after persisting.this.apply(event) internally — that is framework-agnostic.// application/queries/get-<context>.query.ts
import { Query } from '@nestjs/cqrs';
import type { Get<Context>Dto } from '../dtos/get-<context>.dto';
export class Get<Context>Query extends Query<Get<Context>Dto.Output> {
constructor(
public readonly id: string,
public readonly companyId: string,
) {
super();
}
}
// application/queries/get-<context>.handler.ts
import { Inject, NotFoundException } from '@nestjs/common';
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import type { <Context>Repository } from '../../domain/repositories/<context>.repository';
import { <CONTEXT>_REPOSITORY_TOKEN } from '../../domain/repositories/<context>.repository';
import type { Get<Context>Dto } from '../dtos/get-<context>.dto';
import { Get<Context>Query } from './get-<context>.query';
import { OutputMapper } from '../dtos/<context>-output.dto';
@QueryHandler(Get<Context>Query)
export class Get<Context>Handler
implements IQueryHandler<Get<Context>Query, Get<Context>Dto.Output>
{
constructor(
@Inject(<CONTEXT>_REPOSITORY_TOKEN)
private readonly repository: <Context>Repository.Repository,
) {}
async execute(query: Get<Context>Query): Promise<Get<Context>Dto.Output> {
const entity = await this.repository.findById(query.id);
if (!entity || entity.companyId !== query.companyId) {
throw new NotFoundException();
}
return OutputMapper.toOutput(entity);
}
}
// Handlers are self-registering — just add to providers array:
providers: [
...existingProviders,
Create<Context>Handler,
Get<Context>Handler,
],
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
// CommandBus.execute() return type inferred from Command<T>
const result = await this.commandBus.execute(
new Create<Context>Command(companyId, name),
);
const item = await this.queryBus.execute(
new Get<Context>Query(id, companyId),
);
Full template: references/cqrs-command-query.md
Use when a handler must coordinate multiple dependencies: external port calls, QueryBus resolution, complex enrichment before delegating to a pure UseCase.
The handler receives NestJS DI and instantiates framework-agnostic use cases via new.
The use case returns the entity so the handler can commit domain events:
// application/commands/create-invoice.handler.ts
@CommandHandler(CreateInvoiceCommand)
export class CreateInvoiceHandler
implements ICommandHandler<CreateInvoiceCommand, { id: string }>
{
private readonly useCase: CreateInvoiceUseCase.UseCase;
constructor(
private readonly queryBus: QueryBus,
@Inject(INVOICE_REPOSITORY_TOKEN)
invoiceRepository: InvoiceRepository.Repository,
@Inject(CUSTOMER_PORT_TOKEN)
private readonly customerPort: CustomerPort,
@Inject(LOGGER_PORT_TOKEN)
logger: LoggerPort,
private readonly publisher: EventPublisher,
) {
// Instantiate framework-agnostic use case here
this.useCase = new CreateInvoiceUseCase.UseCase(invoiceRepository, logger);
}
async execute({ input }: CreateInvoiceCommand): Promise<{ id: string }> {
// 1. Resolve external dependencies (ports, QueryBus)
const config = await this.queryBus.execute(
new ResolveConfigQuery(input.companyId),
);
// 2. Validate / enrich
const customer = await this.customerPort.findById(input.customerId);
// 3. Delegate business logic to use case — useCase returns the entity
const entity = await this.useCase.execute({
...input,
config,
customer,
});
// 4. Handler commits domain events (EventPublisher never enters the use case)
this.publisher.mergeObjectContext(entity);
entity.commit();
return { id: entity.id };
}
}
// application/usecases/create-invoice.usecase.ts — NO NestJS imports
export namespace CreateInvoiceUseCase {
export class UseCase {
constructor(
private readonly repository: InvoiceRepository.Repository,
private readonly logger: LoggerPort,
) {}
// Returns the entity so the handler can commit domain events
async execute(input: Input): Promise<InvoiceEntity> {
const entity = InvoiceEntity.create({ ...input }); // entity.apply() happens here
await this.repository.insert(entity);
return entity;
}
}
}
Key rules for Pattern C:
mergeObjectContext + commit.EventPublisher is injected ONLY in the handler.new UseCase(mockRepo).Benefits:
Full template: references/cqrs-orchestrator.md
// application/dtos/create-<context>.dto.ts
export namespace Create<Context>Dto {
export interface Input {
companyId: string;
name: string;
}
export interface Output {
id: string;
}
}
export class Create<Context>Command extends Command<{ id: string }> {
constructor(
public readonly companyId: string,
public readonly data: { name: string; description?: string },
) {
super();
}
}
// application/dtos/<context>-output.dto.ts
export class OutputMapper {
static toOutput(entity: <Context>Entity): <Context>Output {
return {
id: entity.id,
companyId: entity.companyId,
name: entity.name,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
};
}
}
Full template: references/dto-patterns.md
A port is an interface defined by the consumer module in application/ports/. The provider module implements it via an adapter in infrastructure/adapters/.
// <consumer-module>/application/ports/company.port.ts
export const COMPANY_PORT_TOKEN = Symbol('CompanyPort');
export interface CompanyPort {
findById(id: string): Promise<{
id: string;
isActive: boolean;
} | null>;
}
Module wiring in the provider module:
// <provider-module>/infrastructure/<provider>.module.ts
providers: [
CompanyPortAdapter,
{
provide: COMPANY_PORT_TOKEN,
useExisting: CompanyPortAdapter,
},
],
exports: [COMPANY_PORT_TOKEN],
Full template: references/port-patterns.md
Extract reusable logic shared by 2+ handlers into an Application Service. If only one handler uses it, keep it inline.
// application/services/<context>-builder.service.ts
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class <Context>BuilderService {
constructor(
@Inject(SOME_PORT_TOKEN)
private readonly somePort: SomePort,
) {}
async build(input: BuildInput): Promise<BuildOutput> {
// Shared logic
}
}
Register in module providers and inject into handlers via @Inject or constructor injection.
Shared domain logic with no I/O and no framework. Pure class, no decorators.
// domain/services/<context>-resolver.ts
export class <Context>Resolver {
resolve(entity: <Context>Entity, data: ResolveInput): ResolveOutput {
// Pure domain logic
}
}
Instantiate directly in the handler or via a factory in the module provider.
When queries hit Redis instead of Postgres:
@EventsHandler that writes to Redis when domain events fire.Full template: references/read-model-patterns.md
React to domain events dispatched via entity.commit(). Use @EventsHandler, never @OnEvent, for new code.
// infrastructure/listeners/<context>-created.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { <Context>CreatedEvent } from '../../domain/events/<context>-created.event';
@EventsHandler(<Context>CreatedEvent)
export class <Context>CreatedHandler implements IEventHandler<<Context>CreatedEvent> {
handle(event: <Context>CreatedEvent): void {
// side effects: RabbitMQ publish, WebSocket broadcast, Redis projection
}
}
@Injectable / @Inject / NestJS imports in UseCase classes (Pattern A and C use cases).@CommandHandler, @QueryHandler, @EventsHandler) ARE allowed NestJS decorators.companyId in queries and mutations (multi-tenant enforcement).Command<T>, Queries extend Query<T> (NestJS CQRS native).ICommandHandler<Cmd, ReturnType>.this.apply(event) — never addDomainEvent().publisher.mergeObjectContext(entity) + entity.commit() — never pullDomainEvents().useFactory + inject for use cases in module providers — never useClass.application/ports/, not the provider's.entity.commit() called.| File | Pattern |
|---|---|
references/usecase-plain.md | Pattern A full template with tests |
references/cqrs-command-query.md | Pattern B full template with tests |
references/cqrs-orchestrator.md | Pattern C full template with tests |
references/port-patterns.md | Cross-module port contracts |
references/dto-patterns.md | DTO namespaces, inline, output mappers |
references/read-model-patterns.md | Redis R/W separation + projections |
npx claudepluginhub softtor/nestjs-hexagonal --plugin nestjs-hexagonalImplements Clean Architecture, DDD, and Hexagonal Architecture patterns in NestJS/TypeScript apps for complex backend structuring, domain layers with entities/aggregates, ports/adapters, use cases, and refactoring anemic models.
Implements CQRS (Command Query Responsibility Segregation) for scalable architectures. Use when separating read/write models, optimizing query performance, or building event-sourced systems.
Implements CQRS (Command Query Responsibility Segregation) for scalable architectures. Use when separating read/write models, optimizing query performance, or building event-sourced systems.