Контроллеры отвечают за обработку входящих запросов (Request) и возврат ответов (Response) клиенту.

Назначение контроллера - принимать определенные запросы для приложения. Механизм маршрутизации контролирует, какой контроллер получает какие запросы. Часто каждый контроллер имеет более одного маршрута, и разные маршруты могут выполнять разные действия.

Чтобы создать базовый контроллер, мы используем классы и декораторы. Декораторы связывают классы с необходимыми метаданными и позволяют Nest "смапить" маршруты (привязать запросы к соответствующим контроллерам).

Маршрутизация

В следующем примере мы будем использовать декоратор @Controller(), который обязателен для определения базового контроллера. Мы укажем необязательный префикс пути маршрута cats. Использование префикса пути в декораторе @Controller() позволяет нам легко группировать набор связанных маршрутов и минимизировать повторяющийся код. Например, мы можем сгруппировать набор маршрутов, управляющих взаимодействием с сущностью клиента, под маршрутом /customers. В этом случае мы можем указать префикс пути customers в декораторе @Controller(), чтобы не повторять эту часть пути для каждого маршрута в файле.

cats.controller.ts

import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Декоратор @Get() перед методом findAll() указывает Nest на создание обработчика для определенного HTTP-запроса. Имя декоратора соответствует методу запроса HTTP (в данном случае GET) и пути маршрута. Что такое путь маршрута? Путь маршрута для обработчика определяется путем объединения (необязательного) префикса, объявленного для контроллера, и любого пути, указанного в декораторе метода. Поскольку мы объявили префикс для каждого маршрута ( cats) и не добавили никакой информации о пути в декораторе, Nest будет сопоставлять запросы GET /cats с этим обработчиком. Как уже упоминалось, путь включает в себя необязательный префикс пути контроллера и любую строку пути, объявленную в декораторе метода запроса. Например, префикс пути customers в сочетании с декоратором @Get('profile') вызовет данный метод для запроса GET /customers/profile.

В нашем примере выше, когда GET-запрос делается к этой конечной точке, Nest направляет запрос к нашему пользовательскому методу findAll(). Обратите внимание, что имя метода, которое мы выбрали здесь, совершенно произвольно. Очевидно, что мы должны объявить метод для привязки маршрута, но Nest не придает никакого значения выбранному имени метода.

Этот метод вернет код состояния 200 и соответствующий ответ, который в данном случае является просто строкой. Почему так происходит? Чтобы объяснить, мы сначала представим концепцию, что Nest использует два различных варианта для манипулирования ответами:

Стандартный (рекомендуется)Используя этот встроенный метод, когда обработчик запроса возвращает объект JavaScript или массив, он будет автоматически сериализован в JSON. Однако если он возвращает примитивный тип JavaScript (например, string, number, boolean), Nest отправит только значение, не пытаясь его сериализовать. Это делает обработку ответа простой: просто верните значение, а Nest позаботится обо всем остальном.

Кроме того, код состояния ответа по умолчанию всегда равен 200, за исключением POST-запросов, которые используют 201. Мы можем легко изменить это поведение, добавив декоратор @HttpCode(...) на уровне обработчика.
Специфический для библиотекиМы можем использовать специфичный для библиотеки (например, Express) объект ответа, который можно внедрить с помощью декоратора @Res() в аргументы обработчика метода (например, findAll(@Res() response) ). При таком подходе у вас есть возможность использовать собственные методы обработки ответа, представленные этим объектом. Например, в Express вы можете создавать ответы с помощью метода response.status(200).send().

Объект запроса

Обработчикам часто требуется доступ к объекту Request. Nest предоставляет доступ к объекту Request базовой платформы (по умолчанию Express). Мы можем получить доступ к объекту запроса, попросив Nest внедрить его, добавив декоратор @Req() перед обработчиком.

cats.controller.ts

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

Объект request представляет HTTP-запрос и имеет свойства для доступа к параметрам запроса, HTTP-заголовкам и телу запроса. В большинстве случаев нет необходимости получать эти свойства вручную. Вместо этого мы можем использовать специальные декораторы, такие как @Body() или @Query(), которые доступны из коробки. Ниже приведен список предоставляемых декораторов и объектов, специфичных для конкретной платформы, которые они представляют.

@Request(), @Req()req
@Response(), @Res()*res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

* Для совместимости с типами, используемыми в базовых фреймворках HTTP (например, Express и Fastify), Nest предоставляет декораторы @Res() и @Response(). @Res() - это просто псевдоним для @Response(). Оба декоратора по сути дублируют базовый интерфейс объекта response родной платформы. При их использовании следует также импортировать типы для базовой библиотеки (например, @types/express), чтобы воспользоваться всеми преимуществами. Обратите внимание, что когда вы вставляете @Res() или @Response() в обработчик метода, вы переводите Nest в режим, специфичный для библиотеки, и становитесь ответственным за управление ответом. При этом вы должны выдать какой-то ответ, сделав вызов объекта response (например, res.json(...) или res.send(...)), иначе HTTP-сервер зависнет.

Ресурсы

Ранее мы определили маршрут для получения ресурса cats (маршрут GET). Обычно нам так же нужен маршрут, который создает новые записи. Для этого создадим обработчик POST:

cats.controller.ts

import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Все очень просто. Nest предоставляет декораторы для всех стандартных методов HTTP: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), и @Head(). Кроме того, @All() определяет маршрут, который обрабатывает их все.

Шаблоны маршрутов

Поддерживаются также маршруты, основанные на шаблонах. Например, звездочка используется в качестве подстановочного знака и будет соответствовать любой комбинации символов.

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

Маршрутный путь 'ab*cd' будет соответствовать abcd, ab_cd, abecd и так далее. Символы ?, +, * и () могут использоваться в маршрутном пути и являются подмножествами своих аналогов в регулярных выражениях. Дефис (-) и точка (.) интерпретируются буквально в строковых путях.

Код статуса

Как уже упоминалось, код (status code) ответа по умолчанию всегда 200, за исключением POST-запросов, которые отдают 201. Мы можем легко изменить это поведение, добавив декоратор @HttpCode(...) на уровне обработчика.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

Часто status code не статичен, а зависит от различных факторов. В этом случае вы можете использовать специфичный для библиотеки response объект (с помощью @Res()), или в случае ошибки, выбросить исключение.

Заголовки

Чтобы указать пользовательский заголовок ответа, вы можете использовать либо декоратор @Header(), либо специфический для библиотеки объект response (и вызвать res.header() напрямую).

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Редирект

Чтобы сделать редирект ответа на определенный URL, вы можете использовать либо декоратор @Redirect(), либо специфический для библиотеки объект response (и вызвать res.redirect() напрямую).

@Redirect() принимает два аргумента, url и statusCode, оба необязательны. Значение по умолчанию statusCode равно 302 (Found), если оно опущено.

@Get()
@Redirect('https://nestjs.com', 301)

Иногда вам может понадобиться определить HTTP код ответа или URL редиректа динамически. Сделайте это просто вернув подобный объект из метода обработчика маршрута:

{
  "url": string,
  "statusCode": number
}

Возвращаемые значения будут переопределять любые аргументы, переданные декоратору @Redirect(). Например:

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

Параметры маршрута

Маршруты со статическими путями не будут работать, когда вам нужно принять динамические данные как часть запроса (например, GET /cats/1 для получения кошки с id 1). Чтобы определить маршруты с параметрами, мы можем добавить названия параметров в путь маршрута для захвата динамического значения в данной позиции в URL запроса. Название параметра в примере декоратора @Get() ниже демонстрирует такое использование. К параметрам маршрута, объявленным таким образом, можно получить доступ с помощью декоратора @Param(), который следует добавить в аргументы метода.

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param() используется для декорирования параметра метода (params в примере выше) и делает параметры маршрута доступными как свойства этого параметра внутри тела метода. Как видно из приведенного выше кода, мы можем получить доступ к параметру id, обратившись к params.id. Вы также можете передать в декоратор @Param определенное название параметра, а затем ссылаться на параметр маршрута непосредственно по имени в теле метода.

Импортируйте Param из пакета @nestjs/common.

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

Маршрутизация по субдоменам

Декоратор @Controller может принимать опцию host, чтобы проверить, что HTTP-хост входящих запросов соответствует некоторому определенному значению.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

Поскольку Fastify не поддерживает вложенные маршрутизаторы, при использовании поддоменной маршрутизации вместо него следует использовать (по умолчанию) адаптер Express.

Подобно маршрутному пути, параметр hosts может использовать шаблоны для захвата динамического значения в данной позиции имени хоста. Шаблон параметраhost в примере декоратора @Controller() ниже, демонстрирует такое использование. К параметрам хоста, объявленным таким образом, можно получить доступ с помощью декоратора @HostParam(), который должен быть добавлен в аргументы метода.

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

Scopes

Для людей, изучающих разные языки программирования, может удивить, что в Nest почти все разделяется между входящими запросами. У нас есть пул соединений с базой данных, синглтон-сервисы с глобальным состоянием и т.д. Помните, что Node.js не следует Multi-Threaded Stateless Model, в которой каждый запрос обрабатывается отдельным потоком (как в php). Следовательно, использование экземпляров синглтонов полностью безопасно для наших приложений.

Однако существуют случаи, когда время жизни контроллера только на время запроса может быть нужно, например, кэширование по каждому запросу в GraphQL-приложениях, отслеживание запросов или многопоточность. Узнайте, как управлять scopes [здесь] (/fundamentals/injection-scopes).

Асинхронность

Мы любим современный JavaScript и знаем, что извлечение данных в основном асинхронно. Поэтому Nest поддерживает и хорошо работает с синтаксисом async.

Каждая асинхронная функция должна возвращать Promise. Это означает, что вы можете вернуть deferred значение, которое Nest сможет зарезолвить самостоятельно. Давайте посмотрим пример этого:

cats.controller.ts

@Get()
async findAll(): Promise<any[]> {
  return [];
}

Приведенный выше код полностью корректен. Более того, обработчики маршрутов Nest стали еще мощнее, поскольку могут возвращать RxJS observable streams (opens new window). Nest автоматически подпишется на источник и возьмет последнее выданное значение (как только поток будет завершен).

cats.controller.ts

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

Оба вышеуказанных подхода работают, и вы можете использовать тот, который соответствует вашим требованиям.

Тело запроса

Наш предыдущий пример обработчика маршрута POST не принимал никаких данных от клиента. Давайте исправим это, добавив сюда декоратор @Body().

Но сначала (если вы используете TypeScript) нам нужно определить схему DTO (Data Transfer Object). DTO - это объект, который определяет, как данные будут передаваться по сети. Мы можем определить схему DTO, используя TypeScript интерфейсы или простые классы. Интересно, что здесь мы рекомендуем использовать классы. Почему? Классы являются частью стандарта JavaScript ES6, и поэтому они сохраняются как реальные сущности в скомпилированном JavaScript. С другой стороны, поскольку интерфейсы TypeScript удаляются при трайнспайлинге, Nest не может ссылаться на них во время выполнения. Это важно, поскольку такие функции, как Pipes, предоставляют дополнительные возможности, когда они имеют доступ к метатипу переменной во время выполнения.

Давайте создадим класс CreateCatDto:

create-cat.dto

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

Он имеет только три основных свойства. После этого мы можем использовать созданный DTO внутри CatsController:

create-cat.dto

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

Наш ValidationPipe может отфильтровывать свойства, которые не должны быть получены обработчиком метода. В этом случае мы можем составить белый список допустимых свойств, и любое свойство, не включенное в белый список, будет автоматически удалено из результирующего объекта. В примере CreateCatDto, наш белый список - это свойства name, age и breed.

Пример контроллера

Ниже приведен пример, который использует несколько доступных декораторов для создания базового контроллера. Этот контроллер предоставляет методы для доступа и манипулирования внутренними данными.

cats.controller.ts

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';
@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }
  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }
  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }
  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }
  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

Начало работы

Когда мы полностью описали наш контроллер - Nest все еще не знает о существовании CatsController и, как следствие, не создает экземпляр этого класса.

Контроллеры всегда связаны с модулем, поэтому мы включаем массив controllers в декоратор @Module(). Поскольку мы еще не определили никаких других модулей, кроме корневого AppModule, мы используем его для привязки CatsController:

@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
  controllers: [CatsController],
})
export class AppModule {}

Мы прикрепили метаданные к классу модуля с помощью декоратора @Module(), и теперь Nest может понять, какие контроллеры должны быть смонтированы.

Подход с учетом специфики библиотеки

До сих пор мы обсуждали стандартный способ манипулирования ответами Nest. Второй способ манипулирования ответом заключается в использовании специфического для библиотеки объекта response. Чтобы внедрить конкретный объект ответа, нам нужно использовать декоратор @Res(). Чтобы показать разницу, давайте перепишем CatsController следующим образом:

cats.controller.ts

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }
  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

Хотя этот подход работает и в некоторых случаях позволяет добиться большей гибкости, предоставляя полный контроль над объектом ответа (манипуляции с заголовками, специфические для библиотеки функции и так далее), его следует использовать с осторожностью. В целом, этот подход гораздо менее понятен и имеет некоторые недостатки. Главный недостаток заключается в том, что ваш код становится платформозависимым (поскольку базовые библиотеки могут иметь различные API для объекта ответа), и его сложнее тестировать.

Кроме того, в приведенном примере вы теряете совместимость с функциями Nest, которые зависят от стандартной обработки ответов Nest, такими как перехватчики и декораторы @HttpCode() / @Header(). Чтобы исправить это, вы можете установить опцию passthrough в значение true, как показано ниже:

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

Теперь вы можете взаимодействовать с родным объектом ответа (например, устанавливать cookies или заголовки в зависимости от определенных условий), но все остальное предоставьте фреймворку.