AgentStack
SKILL verified MIT Self-run

Infrastructure

skill-softtor-nestjs-hexagonal-infrastructure · by Softtor

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.

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add skill-softtor-nestjs-hexagonal-infrastructure

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

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

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README — it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-softtor-nestjs-hexagonal-infrastructure)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.

Preview Execution monitoring

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 →
Are you the author of Infrastructure? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

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

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

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

// 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:

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

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

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

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

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.