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

NESTJS · HTTP Фаза 7. Контроллеры и роутинг

Контроллеры и роутинг в NestJS

TypeScript · NestJS

Маршруты, параметры, DTO, статусы и тонкая граница HTTP перед сервисом.

1. Декораторы связывают URL с методом

1 / 5

@Controller задаёт префикс, а @Get, @Post и другие методы - конкретный маршрут. Nest вызывает соответствующий обработчик.

  • Путь содержит ресурс, не действие.
  • Метод HTTP описывает операцию.
  • Маршрут не содержит бизнес-правил.
GET /orders/42 POST /orders OrdersControllerfindOne()create() OrderService

2. Param, Query и Body разделяют вход

2 / 5

Path идентифицирует ресурс, query меняет выборку, body содержит команду. DTO описывает и проверяет форму входа.

  • Param передаёт идентификатор.
  • Query фильтрует и пагинирует.
  • Body валидируется до сервиса.
@Param("id") @Query("page") @Body() dto Validated inputid, page, itemscorrect types Service

3. HttpCode делает протокол явным

3 / 5

Создание возвращает 201, чтение - 200, удаление часто - 204. Код статуса отражает результат, а не удобство метода.

  • Не возвращайте 200 на любой исход.
  • Ошибка маппируется фильтром.
  • Location может указывать новый ресурс.
POST /orderscreated order 42 @HttpCode(201)CreatedLocation: /orders/42 201 Created

4. Контроллер координирует, сервис решает

4 / 5

Контроллер извлекает DTO, вызывает use case и возвращает результат. Проверка запаса, цена и переходы заказа остаются доменной логикой.

  • Не храните состояние в controller.
  • Сервис получает зависимости через DI.
  • Один use case имеет одну границу.
ControllerHTTP only OrderServicebusiness rulescreate order ResponseDTO

5. HTTP тест проверяет границу

5 / 5

supertest вызывает маршрут и проверяет статус, тело и ошибки. Сервис подменяется при MVC slice, интеграционный тест идёт с реальными зависимостями.

  • Проверяйте валидный и невалидный вход.
  • Не повторяйте unit tests сервиса.
  • Проверяйте auth и формат ошибки.
supertestreal HTTP route Nest routevalid → 201invalid → 400 Assertionsbody + status

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

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

А где Express и Fastify

Express это минимальный каркас: маршруты, объекты запроса и ответа и цепочка middleware, функций вида (req, res, next), через которые проходит каждый запрос. Middleware бывают уровня приложения, уровня роутера, обработчики ошибок с четырьмя аргументами и встроенные вроде express.json; логирование запросов делает morgan или pino-http. Всё остальное, валидацию, внедрение зависимостей, структуру проекта, команда придумывает сама, и в большом проекте это заканчивается самодельным каркасом. Fastify делает то же быстрее и со схемами валидации из коробки.

NestJS стоит поверх одного из них: по умолчанию внутри Express, флагом переключается на Fastify. Он добавляет то, чего в Express нет: модули и контейнер зависимостей, декораторы для маршрутов и параметров, pipes для валидации, guards, interceptors и фильтры исключений, то есть единую структуру, которую новый разработчик узнаёт в любом проекте на NestJS. Честный ответ на вопрос «чем Express отличается от NestJS»: Express это библиотека для HTTP, NestJS это архитектура приложения, которая использует Express как транспорт.

Маршруты и 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 внедряет их автоматически.
  • Express это маршруты и цепочка middleware (req, res, next) без структуры, Fastify то же быстрее; NestJS работает поверх любого из них и добавляет модули, DI, pipes, guards, interceptors и фильтры.

Что пощупать

Контроллер каталога в практикуме remodov/marketplace-system-nodejs умещается в один класс: @Get() с фильтром, @Get(':id'), @Post() и @Post(':id/reserve') с @HttpCode(200), потому что резерв не создаёт ресурс. Идентификатор проходит через ParseUUIDPipe со своим exceptionFactory, и мусор в пути получает 400 в формате Problem Details, а не стандартный ответ NestJS.

Код: product.controller.ts.

Сделаем сами

Ветка step-02-read-endpoint - добавить параметр maxPrice и выбор сценария в контроллере.

Что почитать дальше