Контроллер в 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 и валидация, — и продукт-инженер не размазывает правила по краю.