# Infrastructure

> 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.

- **Type:** Skill
- **Install:** `agentstack add skill-softtor-nestjs-hexagonal-infrastructure`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Softtor](https://agentstack.voostack.com/s/softtor)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Softtor](https://github.com/Softtor)
- **Source:** https://github.com/Softtor/nestjs-hexagonal/tree/main/skills/infrastructure
- **Website:** https://github.com/Softtor/nestjs-hexagonal#readme

## Install

```sh
agentstack add skill-softtor-nestjs-hexagonal-infrastructure
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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/
├── .module.ts           # @Module wiring — exports ONLY port tokens
├── controllers/
│   ├── .controller.ts
│   └── dtos/                     # Request DTOs — class-validator lives here only
│       └── __tests__/
├── database/
│   ├── prisma/
│   │   ├── models/
│   │   │   └── -model.mapper.ts   # static toEntity() / toModel()
│   │   └── repositories/
│   │       ├── prisma-.repository.ts
│   │       └── __tests__/
│   └── in-memory/
│       └── repositories/
│           └── -in-memory.repository.ts   # for unit tests
├── adapters/
│   └── .adapter.ts      # implements cross-module port interface
└── listeners/
    └── -created.handler.ts  # @EventsHandler — side effects only
```

## 1. Prisma Repository

Pure persistence. No domain logic, no event dispatch.

```typescript
// infrastructure/database/prisma/repositories/prisma-.repository.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/shared/infrastructure/database/prisma.service';
import { Entity } from '../../domain/entities/.entity';
import type { Repository } from '../../domain/repositories/.repository';

@Injectable()
export class PrismaRepository implements Repository.Repository {
  sortableFields: string[] = ['name', 'createdAt'];

  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string): PromiseEntity | null> {
    const record = await this.prisma..findUnique({ where: { id } });
    if (!record) return null;
    return ModelMapper.toEntity(record);
  }

  async save(entity: Entity): Promise {
    const data = ModelMapper.toModel(entity);
    const existing = await this.prisma..findUnique({ where: { id: entity.id } });
    if (existing) {
      await this.prisma..update({ where: { id: entity.id }, data });
    } else {
      await this.prisma..create({ data });
    }
  }

  async search(
    props: Repository.SearchParams,
  ): PromiseRepository.SearchResult> {
    const sortable = this.sortableFields.includes(props.sort ?? '') || false;
    const orderByField = sortable ? props.sort : 'createdAt';
    const orderByDir = sortable ? props.sortDir : 'desc';

    const whereClause: Record = {
      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..count({ where: whereClause }),
      this.prisma..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 Repository.SearchResult({
      items: records.map(ModelMapper.toEntity),
      total: count,
      currentPage: props.page,
      perPage: props.perPage,
      sort: orderByField,
      sortDir: orderByDir,
      filter: props.filter,
    });
  }

  async delete(id: string, organizationId: string): Promise {
    await this.prisma..delete({ where: { id, organizationId } });
  }
}
```

See full template: `references/prisma-repository.md`

## 2. Model Mapper

Bridges Prisma models and domain entities. Static methods only.

```typescript
// infrastructure/database/prisma/models/-model.mapper.ts
import { Entity } from '../../../domain/entities/.entity';
import type {  } from '@prisma/client';

export class ModelMapper {
  static toEntity(model: ): Entity {
    return Entity.restore(
      {
        organizationId: model.organizationId,
        name: model.name,
        createdAt: model.createdAt,
        updatedAt: model.updatedAt ?? undefined,
      },
      model.id,
    );
  }

  static toModel(entity: Entity): Omit, 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.

```typescript
// infrastructure/database/in-memory/repositories/-in-memory.repository.ts
import { Entity } from '../../../domain/entities/.entity';
import type { Repository } from '../../../domain/repositories/.repository';

export class InMemoryRepository implements Repository.Repository {
  readonly items: MapEntity> = new Map();
  sortableFields: string[] = ['name', 'createdAt'];

  async findById(id: string): PromiseEntity | null> {
    return this.items.get(id) ?? null;
  }

  async save(entity: Entity): Promise {
    this.items.set(entity.id, entity);
  }

  async search(
    props: Repository.SearchParams,
  ): PromiseRepository.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 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 {
    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.

```typescript
// infrastructure/.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { DatabaseModule } from '@/shared/infrastructure/database/database.module';
import { _REPOSITORY } from '../domain/repositories/.repository';
import { PrismaRepository } from './database/prisma/repositories/prisma-.repository';
import { CreateHandler } from '../application/commands/create-.handler';
import { GetHandler } from '../application/queries/get-.handler';
import { CreatedHandler } from './listeners/-created.handler';
import { Controller } from './controllers/.controller';

@Module({
  imports: [CqrsModule, DatabaseModule],
  controllers: [Controller],
  providers: [
    PrismaRepository,
    { provide: _REPOSITORY, useExisting: PrismaRepository },
    CreateHandler,
    GetHandler,
    CreatedHandler,
  ],
  exports: [_REPOSITORY],  // NEVER export use cases or repositories
})
export class Module {}
```

For plain UseCase pattern (non-CQRS), inject via `useFactory`:

```typescript
{
  provide: CREATE__USE_CASE_TOKEN,
  useFactory: (repo: Repository.Repository) =>
    new CreateUseCase(repo),
  inject: [_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.

```typescript
// infrastructure/adapters/.adapter.ts
import { Injectable } from '@nestjs/common';
import {  } from '@//application/ports/.port';
import { Service } from '../services/.service';

@Injectable()
export class Adapter implements  {
  constructor(private readonly service: Service) {}

  async findById(id: string): Promise {
    const entity = await this.service.findById(id);
    return entity ? this.mapToProps(entity) : null;
  }

  private mapToProps(entity: Entity): ExternalPortProps {
    return { id: entity.id, name: entity.name };
  }
}

// In module providers:
// Adapter,
// { provide: '', useExisting: Adapter },
// exports: [''],
```

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.

```typescript
// infrastructure/listeners/-created.handler.ts
import { Logger } from '@nestjs/common';
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { CreatedEvent } from '../../domain/events/-created.event';

@EventsHandler(CreatedEvent)
export class CreatedHandler implements IEventHandlerCreatedEvent> {
  private readonly logger = new Logger(CreatedHandler.name);

  async handle(event: CreatedEvent): Promise {
    try {
      // side effects: publish to RabbitMQ, broadcast via WebSocket, update Redis projection
      this.logger.log(`Handling CreatedEvent for ${event.entityId}`);
    } catch (error) {
      this.logger.error(
        `Failed to handle 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

- [ ] Prisma repository: `@Injectable()`, implements domain repository interface
- [ ] `save()` uses upsert (findById check + create/update), not bare `upsert`
- [ ] `search()` always scopes by `organizationId` in whereClause
- [ ] Model mapper: `toEntity()` calls `Entity.restore()`, `toModel()` returns plain data
- [ ] In-memory repository: state in `Map`, no external dependencies
- [ ] Module imports `CqrsModule` + `DatabaseModule` (or equivalent)
- [ ] Module providers: Prisma repo class + `{ provide: TOKEN, useExisting: ... }`
- [ ] CQRS handlers listed in providers (auto-registered via `@CommandHandler`/`@QueryHandler`)
- [ ] Event handlers listed in providers (auto-registered via `@EventsHandler`)
- [ ] Module `exports` contains ONLY the port token Symbol(s)
- [ ] Adapter class: `@Injectable()`, implements external port interface
- [ ] Adapter registered: `{ provide: TOKEN, useExisting: Adapter }` + exported
- [ ] Event handlers: `try/catch` with logger, never re-throw
- [ ] Run `pnpm lint && pnpm check-types` before committing

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Softtor](https://github.com/Softtor)
- **Source:** [Softtor/nestjs-hexagonal](https://github.com/Softtor/nestjs-hexagonal)
- **License:** MIT
- **Homepage:** https://github.com/Softtor/nestjs-hexagonal#readme

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-softtor-nestjs-hexagonal-infrastructure
- Seller: https://agentstack.voostack.com/s/softtor
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
