NestJS / 38 минут

Контроллеры и сервисы

Разделяйте HTTP-представление и сценарии приложения.

В процессе Теория: впереди Практика: впереди Тест: впереди
04 / 14
После этой главы
  • Разделяйте HTTP-представление и сценарии приложения.
  • Применить тему в задаче «Контроллеры и сервисы» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

HTTP как внешний контракт

Контроллер связывает HTTP-запрос с возможностью приложения. Его контракт включает метод, путь, параметры, тело, статус и форму ответа. Controller задаёт общий префикс, Get или Post — конкретный маршрут. Nest регистрирует методы при сборке приложения; обычный вызов метода класса сам по себе не отправляет HTTP-запрос и не запускает guards или pipes.

TypeScript
@Controller('books')
class BooksController {
  @Get('status')
  status() { return { available: true }; }
}

Пример обслуживает GET /books/status. Контроллер нужно указать в controllers модуля. Префикс приложения, если он задан через setGlobalPrefix, добавляется отдельно. При проверке пути учитывайте все уровни: префикс, Controller и метод. Это помогает понять 404 без случайного изменения бизнес-логики.

Параметры и границы ввода

Param читает именованный параметр маршрута, Query — строку запроса, Body — разобранное тело. Декоратор выбирает источник данных, но тип TypeScript не доказывает правильность внешнего значения. id: number без pipe не превращает строку пути в число. DTO с ValidationPipe рассматриваются в следующих главах.

TypeScript
@Controller('books')
class LookupController {
  @Get(':id')
  find(@Param('id') id: string, @Query('lang') lang?: string) {
    return { id, lang: lang ?? 'ru' };
  }
}

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

Сценарий принадлежит сервису

Контроллер остаётся небольшим: принимает проверенные значения, вызывает сервис, формирует внешний ответ. Сервис выполняет сценарий и управляет нужными зависимостями. Такой код проще проверять без HTTP, повторно использовать в очереди и изменять при смене транспорта. Сервис не должен зависеть от response Express ради отправки статуса.

TypeScript
@Injectable()
class TitlesService {
  find(id: string) { return { id, title: 'JavaScript' }; }
}
@Controller('titles')
class TitlesController {
  constructor(private readonly titles: TitlesService) {}
  @Get(':id')
  find(@Param('id') id: string) { return this.titles.find(id); }
}

В модуле зарегистрируйте TitlesService как provider, а TitlesController как controller. Если сервис расположен в другом модуле, используйте imports и exports вместо создания второго экземпляра в контроллере. В маленькой задаче closure может передать учебные данные, но реальное хранилище принадлежит адаптеру, а не файлу HTTP-маршрутов.

Ответ и статус

При стандартной обработке Nest сериализует возвращённое значение. Можно вернуть Promise: framework дождётся результата. Для GET успешный статус обычно 200, для POST по умолчанию 201. Если контракт требует другого, используйте HttpCode явно. Отсутствующий ресурс оформляйте соответствующим исключением, а не успешным ответом с текстом «ошибка».

TypeScript
@Post()
@HttpCode(201)
create(@Body() input: CreateBookDto) {
  return this.books.create(input);
}

Здесь books и CreateBookDto предполагаются частью соответствующей feature. Если внедрить нативный Res и самостоятельно отправлять ответ, меняется стандартный путь обработки. Это полезно для отдельных случаев, но легко мешает преобразованию ответа interceptors. Не смешивайте return объекта и ручной res.send без понимания режима passthrough и ответственности за завершение.

Публичные поля выбирают явно

Запись аккаунта в БД содержит больше, чем разрешено клиенту: парольный хеш, служебные отметки, настройки восстановления. Возврат всего объекта сервиса может случайно раскрыть их после изменения схемы. Постройте публичный DTO с явным списком полей. Удаление только password не защищает от появления нового приватного поля завтра.

Для списка пользователей и личного профиля могут требоваться разные ответы. Тип возвращаемого значения TypeScript помогает разработчику, но не вырезает лишние свойства во время исполнения. Если применяете сериализацию декораторами, проверьте реальное поведение на HTTP-ответе и зарегистрируйте нужный interceptor. Простая явная проекция часто понятнее сложной магии.

Практика публичного маршрута

Создайте настоящий GET /public. Из переданного user верните только id и email, исключив passwordHash. В solve соберите приложение, вызовите init, отправьте запрос через Supertest к getHttpServer и верните body. finally с app.close нужен даже при ошибке проверки. Постоянный порт в учебной среде открывать не требуется.

Проверьте не только правильные значения, но и отсутствие приватного свойства. В итоговой библиотеке разделите контроллеры книг и аккаунтов, держите операции с БД в сервисах и репозиториях. Затем добавьте проверки 404, неверного параметра и запрета доступа. Они подтверждают HTTP-контракт, который невозможно проверить одним прямым вызовом метода сервиса.

Загружаем редактор…
Следующая темаDTO и ValidationPipe

Музыка: Reverie — Scott Buckley · CC BY 4.0