Install
$ agentstack add skill-softtor-nestjs-hexagonal-application ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README — it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
> Note: Examples use companyId as the multi-tenant identifier. Replace with your project's term (e.g., organizationId, tenantId).
Application Layer
This skill covers everything inside a bounded context's application/ directory: use cases, CQRS handlers, DTOs, ports, application services, and Redis read models.
1. Pattern Selection
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/)
2. Pattern A — Plain UseCase with TOKEN
Use when the context uses plain use cases without CQRS (e.g., customers, contacts).
Directory layout
application/
├── dtos/
│ └── create-.dto.ts
└── usecases/
├── create-.usecase.ts
└── __tests__/
└── create-.usecase.spec.ts
DTO
// application/dtos/create-.dto.ts
export namespace CreateDto {
export interface Input {
companyId: string;
name: string;
// add domain-specific fields
}
export interface Output {
id: string;
companyId: string;
name: string;
createdAt: Date;
}
}
UseCase class
No @Injectable, no NestJS imports. Pure TypeScript class.
// application/usecases/create-.usecase.ts
import type { Repository } from '../../domain/repositories/.repository';
import type { CreateDto } from '../dtos/create-.dto';
import { Entity } from '../../domain/entities/.entity';
import { OutputMapper } from '../dtos/-output.dto';
export const CREATE__USE_CASE_TOKEN = Symbol('CreateUseCase');
export namespace CreateUseCase {
export type Input = CreateDto.Input;
export type Output = CreateDto.Output;
export class UseCase {
constructor(
private readonly repository: Repository.Repository,
) {}
async execute(input: Input): Promise {
const entity = Entity.create({
companyId: input.companyId,
name: input.name,
});
await this.repository.insert(entity);
return OutputMapper.toOutput(entity);
}
}
}
Module registration
// infrastructure/.module.ts
import {
CREATE__USE_CASE_TOKEN,
CreateUseCase,
} from '../application/usecases/create-.usecase';
import { _REPOSITORY_TOKEN } from '../domain/repositories/.repository';
@Module({
providers: [
PrismaRepository,
{
provide: _REPOSITORY_TOKEN,
useExisting: PrismaRepository,
},
{
provide: CREATE__USE_CASE_TOKEN,
useFactory: (repo: Repository.Repository) =>
new CreateUseCase.UseCase(repo),
inject: [_REPOSITORY_TOKEN],
},
],
exports: [_REPOSITORY_TOKEN], // NEVER export use cases
})
export class Module {}
Controller injection
constructor(
@Inject(CREATE__USE_CASE_TOKEN)
private readonly createUseCase: CreateUseCase.UseCase,
) {}
Full template: references/usecase-plain.md
3. Pattern B — CQRS Command/Query Handler
Use when the module already integrates CqrsModule. Handlers are self-registering via @CommandHandler / @QueryHandler — no token needed.
Directory layout
application/
├── dtos/
│ ├── create-.dto.ts
│ └── get-.dto.ts
├── commands/
│ ├── create-.command.ts
│ └── create-.handler.ts
└── queries/
├── get-.query.ts
└── get-.handler.ts
Command
// application/commands/create-.command.ts
import { Command } from '@nestjs/cqrs';
import type { CreateDto } from '../dtos/create-.dto';
export class CreateCommand extends CommandDto.Output> {
constructor(
public readonly companyId: string,
public readonly name: string,
) {
super();
}
}
Command Handler
// application/commands/create-.handler.ts
import { Inject } from '@nestjs/common';
import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs';
import type { Repository } from '../../domain/repositories/.repository';
import { _REPOSITORY_TOKEN } from '../../domain/repositories/.repository';
import { Entity } from '../../domain/entities/.entity';
import type { CreateDto } from '../dtos/create-.dto';
import { CreateCommand } from './create-.command';
@CommandHandler(CreateCommand)
export class CreateHandler
implements ICommandHandlerCommand, CreateDto.Output>
{
constructor(
@Inject(_REPOSITORY_TOKEN)
private readonly repository: Repository.Repository,
private readonly publisher: EventPublisher,
) {}
async execute(command: CreateCommand): PromiseDto.Output> {
// Create entity (entity.apply(event) happens inside Entity.create)
const entity = 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:
- Write commands return
voidor{ id: string }— never the full aggregate. EventPublisheris injected ONLY in the handler, never in use cases.mergeObjectContext(entity)+entity.commit()happen in the handler after persisting.- Entity calls
this.apply(event)internally — that is framework-agnostic.
Query
// application/queries/get-.query.ts
import { Query } from '@nestjs/cqrs';
import type { GetDto } from '../dtos/get-.dto';
export class GetQuery extends QueryDto.Output> {
constructor(
public readonly id: string,
public readonly companyId: string,
) {
super();
}
}
Query Handler
// application/queries/get-.handler.ts
import { Inject, NotFoundException } from '@nestjs/common';
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import type { Repository } from '../../domain/repositories/.repository';
import { _REPOSITORY_TOKEN } from '../../domain/repositories/.repository';
import type { GetDto } from '../dtos/get-.dto';
import { GetQuery } from './get-.query';
import { OutputMapper } from '../dtos/-output.dto';
@QueryHandler(GetQuery)
export class GetHandler
implements IQueryHandlerQuery, GetDto.Output>
{
constructor(
@Inject(_REPOSITORY_TOKEN)
private readonly repository: Repository.Repository,
) {}
async execute(query: GetQuery): PromiseDto.Output> {
const entity = await this.repository.findById(query.id);
if (!entity || entity.companyId !== query.companyId) {
throw new NotFoundException();
}
return OutputMapper.toOutput(entity);
}
}
Module registration
// Handlers are self-registering — just add to providers array:
providers: [
...existingProviders,
CreateHandler,
GetHandler,
],
Controller dispatch
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
// CommandBus.execute() return type inferred from Command
const result = await this.commandBus.execute(
new CreateCommand(companyId, name),
);
const item = await this.queryBus.execute(
new GetQuery(id, companyId),
);
Full template: references/cqrs-command-query.md
4. Pattern C — Handler as Orchestrator
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
{
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 {
// 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 {
const entity = InvoiceEntity.create({ ...input }); // entity.apply() happens here
await this.repository.insert(entity);
return entity;
}
}
}
Key rules for Pattern C:
- UseCase returns the entity — handler owns
mergeObjectContext+commit. EventPublisheris injected ONLY in the handler.- UseCase has zero NestJS imports (except entity base class from shared/domain).
- UseCase is testable without NestJS — just
new UseCase(mockRepo).
Benefits:
- UseCase is 100% framework-agnostic — testable without NestJS.
- Handler is a thin wiring layer — testable by mocking only external dependencies.
- UseCase can be reused in other handlers or frameworks.
Full template: references/cqrs-orchestrator.md
5. DTOs
Namespace pattern (preferred for write operations)
// application/dtos/create-.dto.ts
export namespace CreateDto {
export interface Input {
companyId: string;
name: string;
}
export interface Output {
id: string;
}
}
Inline in Command class (acceptable for simple commands)
export class CreateCommand extends Command {
constructor(
public readonly companyId: string,
public readonly data: { name: string; description?: string },
) {
super();
}
}
Output mapper (always extract when mapping is non-trivial)
// application/dtos/-output.dto.ts
export class OutputMapper {
static toOutput(entity: Entity): Output {
return {
id: entity.id,
companyId: entity.companyId,
name: entity.name,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
};
}
}
Full template: references/dto-patterns.md
6. Ports (Cross-Module Communication)
A port is an interface defined by the consumer module in application/ports/. The provider module implements it via an adapter in infrastructure/adapters/.
// /application/ports/company.port.ts
export const COMPANY_PORT_TOKEN = Symbol('CompanyPort');
export interface CompanyPort {
findById(id: string): Promise;
}
Module wiring in the provider module:
// /infrastructure/.module.ts
providers: [
CompanyPortAdapter,
{
provide: COMPANY_PORT_TOKEN,
useExisting: CompanyPortAdapter,
},
],
exports: [COMPANY_PORT_TOKEN],
Full template: references/port-patterns.md
7. Application Services
Extract reusable logic shared by 2+ handlers into an Application Service. If only one handler uses it, keep it inline.
// application/services/-builder.service.ts
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class BuilderService {
constructor(
@Inject(SOME_PORT_TOKEN)
private readonly somePort: SomePort,
) {}
async build(input: BuildInput): Promise {
// Shared logic
}
}
Register in module providers and inject into handlers via @Inject or constructor injection.
8. Domain Services
Shared domain logic with no I/O and no framework. Pure class, no decorators.
// domain/services/-resolver.ts
export class Resolver {
resolve(entity: Entity, data: ResolveInput): ResolveOutput {
// Pure domain logic
}
}
Instantiate directly in the handler or via a factory in the module provider.
9. Read Model / CQRS R/W Separation
When queries hit Redis instead of Postgres:
- Write side: Command handler → repository → Prisma/PostgreSQL → emit domain event.
- Read side: Query handler → Redis (projected view) → fallback to Prisma on miss.
- Projection updater:
@EventsHandlerthat writes to Redis when domain events fire.
Full template: references/read-model-patterns.md
10. Event Handlers
React to domain events dispatched via entity.commit(). Use @EventsHandler, never @OnEvent, for new code.
// infrastructure/listeners/-created.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { CreatedEvent } from '../../domain/events/-created.event';
@EventsHandler(CreatedEvent)
export class CreatedHandler implements IEventHandlerCreatedEvent> {
handle(event: CreatedEvent): void {
// side effects: RabbitMQ publish, WebSocket broadcast, Redis projection
}
}
11. Rules
- No
@Injectable/@Inject/ NestJS imports in UseCase classes (Pattern A and C use cases). - CQRS handlers (
@CommandHandler,@QueryHandler,@EventsHandler) ARE allowed NestJS decorators. - Always include
companyIdin queries and mutations (multi-tenant enforcement). - Commands extend
Command, Queries extendQuery(NestJS CQRS native). - Handlers specify full return type:
ICommandHandler. - Entity emits events via
this.apply(event)— neveraddDomainEvent(). - Handler calls
publisher.mergeObjectContext(entity)+entity.commit()— neverpullDomainEvents(). - Modules export only PORT tokens — never use cases, never Prisma repositories.
useFactory+injectfor use cases in module providers — neveruseClass.- Ports are defined in the consumer module's
application/ports/, not the provider's.
12. Testing Checklist
- [ ] UseCase unit test: mock repository, assert output shape.
- [ ] Command handler test: mock repository + EventPublisher, assert
entity.commit()called. - [ ] Query handler test: mock repository, assert output mapper applied.
- [ ] Port test: mock adapter, assert interface contract.
- [ ] Application service test: mock ports, test shared logic in isolation.
- [ ] Read model test: mock Redis port, assert fallback to Prisma on miss.
Reference Files
| 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 f
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Softtor
- Source: Softtor/nestjs-hexagonal
- License: MIT
- Homepage: https://github.com/Softtor/nestjs-hexagonal#readme
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.