Install
$ agentstack add skill-softtor-nestjs-hexagonal-presentation ✓ 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 No
- ✓ 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
Presentation Layer
The presentation layer is the HTTP boundary. It owns input validation, Swagger docs, and error mapping. It must not contain business logic.
Directory Structure
infrastructure/
└── controllers/
├── .controller.ts
├── __tests__/
│ └── .controller.spec.ts
└── dtos/
├── create-.request.dto.ts
├── update-.request.dto.ts
├── list-.request.dto.ts
└── __tests__/
└── create-.request.dto.spec.ts
Or as a top-level presentation/ folder when separated from infrastructure/:
presentation/
├── controllers/
│ └── .controller.ts
├── dtos/
│ └── create-.http-dto.ts
├── presenters/
│ └── create-.presenter.ts # maps use case output to HTTP response
└── validators/
└── .validator.ts # @ValidatorConstraint classes
1. Controller Pattern
Use CommandBus / QueryBus for CQRS contexts. Use @Inject(TOKEN) for plain use case contexts.
// controllers/.controller.ts
import {
Controller, Post, Get, Patch, Delete, Body, Param, Query,
HttpCode, HttpStatus, UseGuards,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import {
ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiBody,
} from '@nestjs/swagger';
import { AuthGuard } from '@/auth/guards/auth.guard';
import { CurrentUser, User } from '@/auth/decorators/current-user.decorator';
import { CurrentOrganization, OrgContext } from '@/auth/decorators/current-organization.decorator';
import { CreateRequestDto } from './dtos/create-.request.dto';
import { CreateCommand } from '../../application/commands/create-.command';
import { GetQuery } from '../../application/queries/get-.query';
import { ListQuery } from '../../application/queries/list-.query';
@ApiTags('')
@Controller('')
@UseGuards(AuthGuard)
@ApiBearerAuth()
export class Controller {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new ' })
@ApiResponse({ status: 201, description: ' created' })
@ApiResponse({ status: 400, description: 'Validation error' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async create(
@Body() dto: CreateRequestDto,
@CurrentOrganization() org: OrgContext,
): Promise {
return this.commandBus.execute(
new CreateCommand(org.id, dto.name),
);
}
@Get(':id')
@ApiOperation({ summary: 'Get by id' })
@ApiResponse({ status: 200, description: ' found' })
@ApiResponse({ status: 404, description: ' not found' })
async findOne(
@Param('id') id: string,
@CurrentOrganization() org: OrgContext,
) {
return this.queryBus.execute(new GetQuery(id, org.id));
}
@Get()
@ApiOperation({ summary: 'List with pagination' })
async findAll(
@Query() query: ListRequestDto,
@CurrentOrganization() org: OrgContext,
) {
return this.queryBus.execute(
new ListQuery({ organizationId: org.id, ...query }),
);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete a ' })
@ApiResponse({ status: 204, description: 'Deleted' })
async remove(
@Param('id') id: string,
@CurrentOrganization() org: OrgContext,
): Promise {
await this.commandBus.execute(new DeleteCommand(id, org.id));
}
}
See full template: references/controller-patterns.md
2. Request DTOs
class-validator and class-transformer live ONLY in request DTOs. Never in domain VOs or application services.
// controllers/dtos/create-.request.dto.ts
import { IsString, IsNotEmpty, IsOptional, IsEmail } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateRequestDto {
@ApiProperty({ description: 'Name', example: 'My ' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: 'Description', example: 'Optional description' })
@IsOptional()
@IsString()
description?: string;
}
Global ValidationPipe config (in main.ts):
app.useGlobalPipes(
new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }),
);
See full template: references/request-dto-patterns.md
3. Custom Validators
For cross-field or complex format rules that class-validator built-ins cannot express.
// validators/.validator.ts
import {
ValidatorConstraint, ValidatorConstraintInterface,
ValidationArguments, ValidationOptions, registerDecorator,
} from 'class-validator';
@ValidatorConstraint({ name: '', async: false })
export class Validator implements ValidatorConstraintInterface {
validate(value: unknown, args: ValidationArguments): boolean {
// Return true = valid, false = invalid
return typeof value === 'string' && value.length > 0;
}
defaultMessage(): string {
return ' is invalid';
}
}
export function Is(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [],
validator: Validator,
});
};
}
See full template: references/request-dto-patterns.md (custom validator section)
4. Validation Layers
Each layer has a single validation responsibility. Do not duplicate across layers.
Request DTO (presentation) — format, presence, types (class-validator)
↓
Application DTO (application) — contract between layers (TypeScript interfaces)
↓
Domain VO (domain) — business invariants (manual validate())
↓
Queue Schema (integration) — inter-service contracts (Zod)
See full diagram: references/validation-layers.md
5. Error Filter
Maps domain errors to HTTP status codes. Registered globally.
// infrastructure/filters/http-exception.filter.ts
import {
ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger,
} from '@nestjs/common';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { NotFoundError } from '@/shared/domain/errors/not-found-error';
import { BusinessRuleViolationError } from '@/shared/domain/errors/business-rule-violation.error';
import { ConflictError } from '@/shared/domain/errors/conflict.error';
const ERROR_STATUS_MAP: Array Error, HttpStatus]> = [
[NotFoundError, HttpStatus.NOT_FOUND],
[BusinessRuleViolationError, HttpStatus.UNPROCESSABLE_ENTITY],
[ConflictError, HttpStatus.CONFLICT],
];
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const reply = ctx.getResponse();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
for (const [ErrorClass, httpStatus] of ERROR_STATUS_MAP) {
if (exception instanceof ErrorClass) {
status = httpStatus;
break;
}
}
const message =
exception instanceof Error ? exception.message : 'Internal server error';
if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.error(exception);
}
void reply.status(status).send({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
See full template: references/error-filter-patterns.md
6. Swagger Documentation
Always add @ApiOperation, @ApiResponse, and @ApiProperty. Include request body examples for complex DTOs.
@ApiBody({
type: CreateRequestDto,
examples: {
basic: {
summary: 'Basic example',
value: { name: 'My ' },
},
},
})
Rules
class-validatordecorators ONLY in request DTOs — never in domain VOs or application services.- Global
ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }). - Controller methods contain no business logic — delegate immediately to
CommandBus/QueryBusor use case. - Always include
organizationIdfrom the current auth context — never trust it from the request body. HttpExceptionFiltermaps domain errors to status codes — domain layer never imports NestJS.- All endpoints documented with
@ApiOperationand@ApiResponse.
Checklist
- [ ] Controller:
@ApiTags,@UseGuards,@ApiBearerAuth - [ ] Controller: organization context from
@CurrentOrganization(), not from body/params - [ ] Request DTOs:
class-validatordecorators +@ApiProperty - [ ] Request DTOs:
@IsOptional()before other decorators for optional fields - [ ] Nested DTOs:
@ValidateNested()+@Type(() => NestedDto) - [ ] Array DTOs:
@IsArray()+@ValidateNested({ each: true })+@Type(() => ItemDto) - [ ] Custom validators:
@ValidatorConstraint+registerDecoratorfactory - [ ] Error filter registered globally or at module level
- [ ] Swagger:
@ApiOperation,@ApiResponsefor all endpoints - [ ] Controller tests: mock
CommandBus/QueryBus, test routing and DTO mapping - [ ] DTO tests: use
class-validatorvalidate()to test valid/invalid inputs - [ ] Run
pnpm lint && pnpm check-typesbefore 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
- 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.