Контроллер в NestJS — это вход в сервис по HTTP: он сопоставляет маршруты методам и достаёт из запроса то, что нужно обработчику. Декораторы делают это декларативно, и соблазн — написать в контроллере всю логику. В UCP контроллер остаётся тонким: его дело — перевести HTTP в вызов Handler-а.

Контроллер и маршруты

Класс с @Controller(префикс) группирует маршруты; методы помечаются декораторами HTTP-глаголов.

import { Controller, Get, Post, Param, Body } from '@nestjs/common';

@Controller('products')
export class ProductController {
  constructor(private readonly createProduct: CreateProductHandler) {}

  @Get(':id')
  async getOne(@Param('id') id: string) { /* ... */ }

  @Post()
  async create(@Body() body: CreateProductDto) { /* ... */ }
}

@Controller('products') даёт общий префикс пути; @Get(':id') и @Post() — конкретные маршруты. Контроллер регистрируется в своём модуле (controllers: [ProductController]), а зависимости получает через конструктор.

Параметры запроса

Откуда брать данные, задают декораторы параметров: @Param — из пути, @Query — из строки запроса, @Body — из тела.

import { Controller, Get, Param, Query, ParseIntPipe } from '@nestjs/common';

@Controller('products')
export class ProductController {
  @Get(':id')
  async getOne(@Param('id', ParseIntPipe) id: number) { /* ... */ }

  @Get()
  async list(@Query('category') category: string) { /* ... */ }
}

ParseIntPipe здесь же преобразует строку пути в число и отвергает нечисловое — это pipe на уровне параметра. Тело запроса описывается DTO-классом, который затем валидируется (следующая статья).

Коды ответа и форма

По умолчанию @Post отвечает 201, остальные — 200; явный код задаёт @HttpCode.

import { Controller, Post, Delete, HttpCode, Body, Param } from '@nestjs/common';

@Controller('products')
export class ProductController {
  @Post()
  async create(@Body() body: CreateProductDto): Promise<ProductResponse> { /* ... */ }

  @Delete(':id')
  @HttpCode(204)
  async remove(@Param('id', ParseIntPipe) id: number): Promise<void> { /* ... */ }
}

То, что метод возвращает, NestJS сериализует в тело ответа. Возвращать наружу стоит отдельный response-объект, а не доменную сущность, — чтобы не утекли внутренние поля; преобразование делает class-transformer или явный маппинг.

Тонкий контроллер UCP

Главная дисциплина: контроллер не содержит бизнес-логики. Его три шага — разобрать вход, вызвать Handler, вернуть ответ.

@Controller('products')
export class ProductController {
  constructor(private readonly createProduct: CreateProductHandler) {}

  @Post()
  async create(@Body() body: CreateProductDto): Promise<ProductResponse> {
    const product = await this.createProduct.handle(body.toCommand());
    return ProductResponse.fromDomain(product);
  }
}

Контроллер не знает ни про базу, ни про правила — только про HTTP. Сценарий — в CreateProductHandler, полученном через DI. Это та же граница «контроллер тонкий», что в Spring MVC: когда роутов становятся десятки, именно она держит сервис понятным. А проверку входных данных снимает с контроллера следующий слой — pipes и валидация, — и продукт-инженер не размазывает правила по краю.