Когда браузер или мобильное приложение обращается к серверу, нужно понять: какой код должен ответить на этот конкретный URL? Контроллер в NestJS — это именно то место, где URL соответствует методу. Разберём, как это работает.
Что такое контроллер и зачем он нужен
Раньше, когда писали на чистом Node.js или Express, роутинг выглядел примерно так:
app.get('/products/:id', (req, res) => {
const id = req.params.id;
// здесь же — и логика, и работа с базой, и форматирование ответа
res.json({ id });
});
Всё в одной куче: разбор запроса, бизнес-логика, работа с базой. В маленьком приложении это нормально, но когда маршрутов становится несколько десятков — такой код становится трудночитаемым.
NestJS разделяет эти обязанности. Контроллер отвечает только за одно: принять HTTP-запрос и вернуть HTTP-ответ. Всё остальное — в других классах.
Класс становится контроллером, когда получает декоратор @Controller:
import { Controller, Get } from '@nestjs/common';
@Controller('products')
export class ProductController {
@Get()
findAll() {
return [];
}
}
@Controller('products') задаёт общий префикс: все маршруты внутри этого класса начинаются с /products. @Get() говорит: «этот метод отвечает на GET /products».
Контроллер нужно зарегистрировать в модуле:
@Module({
controllers: [ProductController],
})
export class ProductModule {}
Маршруты и HTTP-методы
NestJS поддерживает все стандартные HTTP-методы через декораторы: @Get, @Post, @Put, @Patch, @Delete. В скобках можно указать дополнительный путь, который добавится к префиксу контроллера:
@Controller('products')
export class ProductController {
@Get()
findAll() { /* GET /products */ }
@Get(':id')
findOne() { /* GET /products/123 */ }
@Post()
create() { /* POST /products */ }
@Delete(':id')
remove() { /* DELETE /products/123 */ }
}
:id — это динамический сегмент пути. NestJS перехватит любое значение на этом месте и сделает его доступным в методе.
Как достать данные из запроса
Из входящего запроса данные могут приходить в трёх местах: в пути (/products/42), в строке запроса (/products?category=shoes) и в теле. Для каждого — свой декоратор параметра.
Из пути — @Param
import { Controller, Get, Param } from '@nestjs/common';
@Controller('products')
export class ProductController {
@Get(':id')
findOne(@Param('id') id: string) {
return { id };
}
}
@Param('id') извлекает значение сегмента :id. По умолчанию это строка. Чтобы сразу получить число и отклонить нечисловой ввод, добавляют ParseIntPipe:
import { Param, ParseIntPipe } from '@nestjs/common';
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return { id };
}
Из строки запроса — @Query
import { Controller, Get, Query } from '@nestjs/common';
@Controller('products')
export class ProductController {
@Get()
findAll(@Query('category') category: string) {
return { category };
}
}
Запрос GET /products?category=shoes передаст строку 'shoes' в параметр category.
Из тела — @Body
import { Controller, Post, Body } from '@nestjs/common';
@Controller('products')
export class ProductController {
@Post()
create(@Body() body: CreateProductDto) {
return body;
}
}
CreateProductDto — это обычный TypeScript-класс, который описывает ожидаемую форму тела. DTO (Data Transfer Object) — просто название для объекта, который переносит данные между частями системы. Как NestJS проверяет корректность данных в DTO — тема отдельной статьи про pipes и валидацию.
Коды ответа
По умолчанию NestJS отвечает кодом 200 для всех методов, кроме POST — там по умолчанию 201 Created. Если нужен другой код, используют @HttpCode:
import { Controller, Delete, HttpCode, Param, ParseIntPipe } from '@nestjs/common';
@Controller('products')
export class ProductController {
@Delete(':id')
@HttpCode(204)
remove(@Param('id', ParseIntPipe) id: number): void {
// 204 No Content — успех, но без тела ответа
}
}
Что метод возвращает — то NestJS и сериализует в JSON-тело ответа. Возвращать наружу стоит отдельный объект-ответ, а не внутреннюю сущность: так не утекут лишние поля.
Зачем контроллер держать «тонким»
Когда контроллер маленький, в него соблазнительно добавить логику прямо здесь — проверить права, запросить базу, посчитать что-то. Поначалу это удобно, но потом контроллер превращается в монолитный файл, который сложно тестировать и изменять.
Устойчивый подход: контроллер делает три шага и только три — разобрать запрос, вызвать нужный класс с логикой, вернуть ответ:
import { Controller, Post, Body } from '@nestjs/common';
@Controller('products')
export class ProductController {
constructor(private readonly productsService: ProductsService) {}
@Post()
async create(@Body() body: CreateProductDto) {
const product = await this.productsService.create(body);
return ProductResponse.from(product);
}
}
Контроллер не знает про базу данных и правила создания товара — это забота ProductsService. Зависимости контроллер получает через конструктор: NestJS сам их создаёт и передаёт через систему DI.
Когда маршрутов становится несколько десятков, именно эта граница держит код понятным: хочешь понять логику — смотри сервис, хочешь понять HTTP-интерфейс — смотри контроллер.
Коротко
@Controller('prefix')задаёт общий префикс пути для всех маршрутов класса.- HTTP-методы — декораторы
@Get,@Post,@Put,@Patch,@Delete; путь уточняется в скобках. @Param— данные из пути,@Query— из строки запроса,@Body— из тела.ParseIntPipeпри параметре преобразует строку в число и отклоняет нечисловой ввод.- По умолчанию
@Postвозвращает 201, остальные — 200; явный код задаёт@HttpCode. - Контроллер не содержит бизнес-логики: принял запрос, вызвал сервис, вернул ответ.
- Зависимости контроллер получает через конструктор — NestJS внедряет их автоматически.
Что почитать дальше
- Модули и DI в NestJS — как NestJS создаёт зависимости и как работают модули.
- Валидация и pipes — как проверить данные из
@Bodyдо того, как они попадут в логику.