| name | infrastructure |
| description | Use when creating infrastructure layer artifacts for a NestJS bounded context — Prisma repositories, in-memory repositories (testing), model mappers, NestJS module wiring, adapters (port implementations), or event handler infrastructure. Repository is pure persistence with no event dispatch. |
| argument-hint | Artifact type or entity (e.g., "repo Order", "module Orders", "adapter PaymentGateway") |
Infrastructure Layer
Infrastructure wires the domain to the outside world: databases, event buses, HTTP frameworks, and external services. It is the only layer allowed to import NestJS decorators and Prisma.
Directory Structure
infrastructure/
├── <context>.module.ts # @Module wiring — exports ONLY port tokens
├── controllers/
│ ├── <context>.controller.ts
│ └── dtos/ # Request DTOs — class-validator lives here only
│ └── __tests__/
├── database/
│ ├── prisma/
│ │ ├── models/
│ │ │ └── <context>-model.mapper.ts # static toEntity() / toModel()
│ │ └── repositories/
│ │ ├── prisma-<context>.repository.ts
│ │ └── __tests__/
│ └── in-memory/
│ └── repositories/
│ └── <context>-in-memory.repository.ts # for unit tests
├── adapters/
│ └── <context>.adapter.ts # implements cross-module port interface
└── listeners/
└── <context>-created.handler.ts # @EventsHandler — side effects only
1. Prisma Repository
Pure persistence. No domain logic, no event dispatch.
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/shared/infrastructure/database/prisma.service';
import { <Context>Entity } from '../../domain/entities/<context>.entity';
import type { <Context>Repository } from '../../domain/repositories/<context>.repository';
@Injectable()
export class Prisma<Context>Repository implements <Context>Repository.Repository {
sortableFields: string[] = ['name', 'createdAt'];
constructor(private readonly prisma: PrismaService) {}
async findById(id: string): Promise<<Context>Entity | null> {
const record = await this.prisma.<context>.findUnique({ where: { id } });
if (!record) return null;
return <Context>ModelMapper.toEntity(record);
}
async save(entity: <Context>Entity): Promise<void> {
const data = <Context>ModelMapper.toModel(entity);
const existing = await this.prisma.<context>.findUnique({ where: { id: entity.id } });
if (existing) {
await this.prisma.<context>.update({ where: { id: entity.id }, data });
} else {
await this.prisma.<context>.create({ data });
}
}
async search(
props: <Context>Repository.SearchParams,
): Promise<<Context>Repository.SearchResult> {
const sortable = this.sortableFields.includes(props.sort ?? '') || false;
const orderByField = sortable ? props.sort : 'createdAt';
const orderByDir = sortable ? props.sortDir : 'desc';
const whereClause: Record<string, unknown> = {
organizationId: props.filter?.organizationId,
};
if (typeof props.filter?.name === 'string') {
whereClause.name = { contains: props.filter.name, mode: 'insensitive' };
}
const [count, records] = await Promise.all([
this.prisma.<context>.count({ where: whereClause }),
this.prisma.<context>.findMany({
where: whereClause,
orderBy: { [orderByField]: orderByDir },
skip: props.page > 0 ? (props.page - 1) * props.perPage : 0,
take: props.perPage > 0 ? props.perPage : 15,
}),
]);
return new <Context>Repository.SearchResult({
items: records.map(<Context>ModelMapper.toEntity),
total: count,
currentPage: props.page,
perPage: props.perPage,
sort: orderByField,
sortDir: orderByDir,
filter: props.filter,
});
}
async delete(id: string, organizationId: string): Promise<void> {
await this.prisma.<context>.delete({ where: { id, organizationId } });
}
}
See full template: references/prisma-repository.md
2. Model Mapper
Bridges Prisma models and domain entities. Static methods only.
import { <Context>Entity } from '../../../domain/entities/<context>.entity';
import type { <Context> } from '@prisma/client';
export class <Context>ModelMapper {
static toEntity(model: <Context>): <Context>Entity {
return <Context>Entity.restore(
{
organizationId: model.organizationId,
name: model.name,
createdAt: model.createdAt,
updatedAt: model.updatedAt ?? undefined,
},
model.id,
);
}
static toModel(entity: <Context>Entity): Omit<<Context>, never> {
return {
id: entity.id,
organizationId: entity.organizationId,
name: entity.name,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt ?? null,
};
}
}
Rule: toEntity() always calls Entity.restore() — never Entity.create().
3. In-Memory Repository
For unit tests. Holds state in a plain Map. No Prisma, no NestJS.
import { <Context>Entity } from '../../../domain/entities/<context>.entity';
import type { <Context>Repository } from '../../../domain/repositories/<context>.repository';
export class <Context>InMemoryRepository implements <Context>Repository.Repository {
readonly items: Map<string, <Context>Entity> = new Map();
sortableFields: string[] = ['name', 'createdAt'];
async findById(id: string): Promise<<Context>Entity | null> {
return this.items.get(id) ?? null;
}
async save(entity: <Context>Entity): Promise<void> {
this.items.set(entity.id, entity);
}
async search(
props: <Context>Repository.SearchParams,
): Promise<<Context>Repository.SearchResult> {
let items = Array.from(this.items.values()).filter(
(e) => e.organizationId === props.filter?.organizationId,
);
if (props.filter?.name) {
const term = props.filter.name.toLowerCase();
items = items.filter((e) => e.name.toLowerCase().includes(term));
}
const total = items.length;
const page = props.page ?? 1;
const perPage = props.perPage ?? 15;
const start = (page - 1) * perPage;
return new <Context>Repository.SearchResult({
items: items.slice(start, start + perPage),
total,
currentPage: page,
perPage,
sort: props.sort ?? null,
sortDir: props.sortDir ?? null,
filter: props.filter ?? null,
});
}
async delete(id: string, organizationId: string): Promise<void> {
const entity = this.items.get(id);
if (entity?.organizationId === organizationId) {
this.items.delete(id);
}
}
}
See full template: references/in-memory-repository.md
4. Module Wiring
Module exports ONLY port tokens. Never use cases, never Prisma classes.
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { DatabaseModule } from '@/shared/infrastructure/database/database.module';
import { <CONTEXT>_REPOSITORY } from '../domain/repositories/<context>.repository';
import { Prisma<Context>Repository } from './database/prisma/repositories/prisma-<context>.repository';
import { Create<Context>Handler } from '../application/commands/create-<context>.handler';
import { Get<Context>Handler } from '../application/queries/get-<context>.handler';
import { <Context>CreatedHandler } from './listeners/<context>-created.handler';
import { <Context>Controller } from './controllers/<context>.controller';
@Module({
imports: [CqrsModule, DatabaseModule],
controllers: [<Context>Controller],
providers: [
Prisma<Context>Repository,
{ provide: <CONTEXT>_REPOSITORY, useExisting: Prisma<Context>Repository },
Create<Context>Handler,
Get<Context>Handler,
<Context>CreatedHandler,
],
exports: [<CONTEXT>_REPOSITORY],
})
export class <Context>Module {}
For plain UseCase pattern (non-CQRS), inject via useFactory:
{
provide: CREATE_<CONTEXT>_USE_CASE_TOKEN,
useFactory: (repo: <Context>Repository.Repository) =>
new Create<Context>UseCase(repo),
inject: [<CONTEXT>_REPOSITORY],
},
See full template: references/module-wiring.md
5. Adapter Pattern
Adapters implement port interfaces defined in other bounded contexts. They translate between the external contract and the local domain.
import { Injectable } from '@nestjs/common';
import { <ExternalPort> } from '@/<other-context>/application/ports/<external>.port';
import { <Context>Service } from '../services/<context>.service';
@Injectable()
export class <Context>Adapter implements <ExternalPort> {
constructor(private readonly service: <Context>Service) {}
async findById(id: string): Promise<ExternalPortProps | null> {
const entity = await this.service.findById(id);
return entity ? this.mapToProps(entity) : null;
}
private mapToProps(entity: <Context>Entity): ExternalPortProps {
return { id: entity.id, name: entity.name };
}
}
See full template: references/adapter-patterns.md
6. Event Handler Infrastructure
Event handlers react to domain events and perform side effects. They must never throw — log errors and continue.
import { Logger } from '@nestjs/common';
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> {
private readonly logger = new Logger(<Context>CreatedHandler.name);
async handle(event: <Context>CreatedEvent): Promise<void> {
try {
this.logger.log(`Handling <Context>CreatedEvent for ${event.entityId}`);
} catch (error) {
this.logger.error(
`Failed to handle <Context>CreatedEvent: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
Use @EventsHandler + IEventHandler for all new code. Never use @OnEvent for domain event handlers.
See full template: references/event-infra-patterns.md
Rules
- Repository is PURE persistence —
save(), findById(), search(), delete(). No event dispatch.
- Events are committed by the Handler via
entity.commit() after repo.save().
- Module exports ONLY port tokens — never use cases, never Prisma repository classes.
- Adapters implement port interfaces with
{ provide: TOKEN, useExisting: Adapter }.
class-validator is ONLY allowed in presentation request DTOs.
- Event handlers use
@EventsHandler + IEventHandler — never @OnEvent for new code.
organizationId in every Prisma query (multi-tenant isolation).
Exception: findById(id) may omit organizationId when used internally after authorization has been verified at the controller/guard level. For user-facing queries, prefer findById(id, organizationId).
- Use
Entity.restore() in mappers — never Entity.create().
Checklist