← назад к разделу

Когда браузер или мобильное приложение обращается к серверу, нужно понять: какой код должен ответить на этот конкретный 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 до того, как они попадут в логику.