agentsclimarketplace

Presentation

Skill Softtor/nestjs-hexagonal/skills/presentation

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.From its SKILL.md

Install
npx -y skills add Softtor/nestjs-hexagonal --skill presentation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.1 KB, as published. Nobody here has run it

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.

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

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

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

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

@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 pnpm lint && pnpm check-types before committing

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.