Skip to content
Back to skills

Presentation

ASecurity

Use when creating presentation layer artifacts for a NestJS bounded context — REST controllers, request DTOs with class-validator, Swagger decorators, custom validators, error filters, or API documentation. Covers the HTTP boundary with proper validation and error mapping.

  • 5 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 26, 2026
developmenttypescriptrustgoexpressapidocumentation

Works with

  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 22, 2026

npx -y skills add Softtor/nestjs-hexagonal --skill presentation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Presentation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Presentation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/softtor-presentation/badge)](https://www.skillsdirectory.com/skills/softtor-presentation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: presentation
description: Use when creating presentation layer artifacts for a NestJS bounded context — REST controllers, request DTOs with class-validator, Swagger decorators, custom validators, error filters, or API documentation. Covers the HTTP boundary with proper validation and error mapping.
argument-hint: Entity or artifact (e.g., "controller Orders", "dto CreateOrder", "error-filter")
---

# 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/
    ├── <context>.controller.ts
    ├── __tests__/
    │   └── <context>.controller.spec.ts
    └── dtos/
        ├── create-<context>.request.dto.ts
        ├── update-<context>.request.dto.ts
        ├── list-<context>.request.dto.ts
        └── __tests__/
            └── create-<context>.request.dto.spec.ts
```

Or as a top-level `presentation/` folder when separated from `infrastructure/`:

```
presentation/
├── controllers/
│   └── <context>.controller.ts
├── dtos/
│   └── create-<context>.http-dto.ts
├── presenters/
│   └── create-<context>.presenter.ts     # maps use case output to HTTP response
└── validators/
    └── <field>.validator.ts              # @ValidatorConstraint classes
```

## 1. Controller Pattern

Use `CommandBus` / `QueryBus` for CQRS contexts. Use `@Inject(TOKEN)` for plain use case contexts.

```typescript
// controllers/<context>.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 { Create<Context>RequestDto } from './dtos/create-<context>.request.dto';
import { Create<Context>Command } from '../../application/commands/create-<context>.command';
import { Get<Context>Query } from '../../application/queries/get-<context>.query';
import { List<Context>Query } from '../../application/queries/list-<context>.query';

@ApiTags('<Contexts>')
@Controller('<contexts>')
@UseGuards(AuthGuard)
@ApiBearerAuth()
export class <Context>Controller {
  constructor(
    private readonly commandBus: CommandBus,
    private readonly queryBus: QueryBus,
  ) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({ summary: 'Create a new <context>' })
  @ApiResponse({ status: 201, description: '<Context> created' })
  @ApiResponse({ status: 400, description: 'Validation error' })
  @ApiResponse({ status: 401, description: 'Unauthorized' })
  async create(
    @Body() dto: Create<Context>RequestDto,
    @CurrentOrganization() org: OrgContext,
  ): Promise<{ id: string }> {
    return this.commandBus.execute(
      new Create<Context>Command(org.id, dto.name),
    );
  }

  @Get(':id')
  @ApiOperation({ summary: 'Get <context> by id' })
  @ApiResponse({ status: 200, description: '<Context> found' })
  @ApiResponse({ status: 404, description: '<Context> not found' })
  async findOne(
    @Param('id') id: string,
    @CurrentOrganization() org: OrgContext,
  ) {
    return this.queryBus.execute(new Get<Context>Query(id, org.id));
  }

  @Get()
  @ApiOperation({ summary: 'List <contexts> with pagination' })
  async findAll(
    @Query() query: List<Context>RequestDto,
    @CurrentOrganization() org: OrgContext,
  ) {
    return this.queryBus.execute(
      new List<Context>Query({ organizationId: org.id, ...query }),
    );
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  @ApiOperation({ summary: 'Delete a <context>' })
  @ApiResponse({ status: 204, description: 'Deleted' })
  async remove(
    @Param('id') id: string,
    @CurrentOrganization() org: OrgContext,
  ): Promise<void> {
    await this.commandBus.execute(new Delete<Context>Command(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.

```typescript
// controllers/dtos/create-<context>.request.dto.ts
import { IsString, IsNotEmpty, IsOptional, IsEmail } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class Create<Context>RequestDto {
  @ApiProperty({ description: 'Name', example: 'My <Context>' })
  @IsString()
  @IsNotEmpty()
  name!: string;

  @ApiPropertyOptional({ description: 'Description', example: 'Optional description' })
  @IsOptional()
  @IsString()
  description?: string;
}
```

Global `ValidationPipe` config (in `main.ts`):

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

```typescript
// validators/<field>.validator.ts
import {
  ValidatorConstraint, ValidatorConstraintInterface,
  ValidationArguments, ValidationOptions, registerDecorator,
} from 'class-validator';

@ValidatorConstraint({ name: '<validatorName>', async: false })
export class <Field>Validator implements ValidatorConstraintInterface {
  validate(value: unknown, args: ValidationArguments): boolean {
    // Return true = valid, false = invalid
    return typeof value === 'string' && value.length > 0;
  }

  defaultMessage(): string {
    return '<Field> is invalid';
  }
}

export function Is<Field>(options?: ValidationOptions) {
  return function (object: object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options,
      constraints: [],
      validator: <Field>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.

```typescript
// 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<[new (...args: never[]) => 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<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();

    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.

```typescript
@ApiBody({
  type: Create<Context>RequestDto,
  examples: {
    basic: {
      summary: 'Basic example',
      value: { name: 'My <Context>' },
    },
  },
})
```

## Rules

- `class-validator` decorators 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`/`QueryBus` or use case.
- Always include `organizationId` from the current auth context — never trust it from the request body.
- `HttpExceptionFilter` maps domain errors to status codes — domain layer never imports NestJS.
- All endpoints documented with `@ApiOperation` and `@ApiResponse`.

## Checklist

- [ ] Controller: `@ApiTags`, `@UseGuards`, `@ApiBearerAuth`
- [ ] Controller: organization context from `@CurrentOrganization()`, not from body/params
- [ ] Request DTOs: `class-validator` decorators + `@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` + `registerDecorator` factory
- [ ] Error filter registered globally or at module level
- [ ] Swagger: `@ApiOperation`, `@ApiResponse` for all endpoints
- [ ] Controller tests: mock `CommandBus`/`QueryBus`, test routing and DTO mapping
- [ ] DTO tests: use `class-validator` `validate()` to test valid/invalid inputs
- [ ] Run `<runner> lint && <runner> check-types` before committing (`<runner>` from the lockfile, see "Package runner" in `nestjs-hexagonal:using-nestjs-hexagonal`)

Files in this skill

  • SKILL.md10.1 KB
  • references/controller-patterns.md8.9 KB
  • references/error-filter-patterns.md8.6 KB
  • references/request-dto-patterns.md8.7 KB
  • references/validation-layers.md8.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.