Собеседования

NestJS на собеседовании: модули, DI, guards, pipes и жизненный цикл запроса

Что спрашивают про NestJS: модули и внедрение зависимостей, порядок работы middleware, guards, interceptors и pipes. Примеры кода и ответы.

NestJS - основной фреймворк в backend-треке на Node.js, и на собеседовании о нём спрашивают предсказуемо: архитектура модулей, внедрение зависимостей и порядок обработки запроса. Ответ «я писал контроллеры» звучит слабо. Сильный ответ объясняет, какую проблему решает каждый элемент и где его место в жизненном цикле.

Модули и провайдеры

Приложение Nest - граф модулей. Модуль объявляет контроллеры (принимают запросы), провайдеры (сервисы, репозитории, фабрики) и решает, что экспортировать. Провайдер, не указанный в exports, недоступен другим модулям - это граница, которая не даёт превратить проект в клубок взаимных импортов.

JavaScript// Модуль собирает провайдеры и решает, что видно снаружи import { Module } from '@nestjs/common' import { UsersController } from './users.controller' import { UsersService } from './users.service' @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], }) export class UsersModule {}

Внедрение зависимостей

Контроллер не создаёт сервис сам - он объявляет зависимость в конструкторе, а DI-контейнер Nest находит провайдер по токену (обычно классу) и передаёт экземпляр. Это упрощает тесты: в тестовом модуле провайдер можно заменить заглушкой. По умолчанию провайдер - синглтон на всё приложение. Scope REQUEST создаёт экземпляр на каждый запрос, TRANSIENT - для каждого потребителя.

TypeScript// Контроллер: pipe преобразует параметр, сервис приходит через DI import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } from '@nestjs/common' import { ApiKeyGuard } from './api-key.guard' import { UsersService } from './users.service' @Controller('users') @UseGuards(ApiKeyGuard) export class UsersController { constructor(private readonly users: UsersService) {} @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number) { const user = await this.users.findById(id) if (!user) throw new NotFoundException('Пользователь не найден') return user } }

ParseIntPipe превращает строку из URL в число и при неверном значении сам вернёт 400 - обработчик не получит мусор. NotFoundException превращается в ответ 404 встроенным фильтром исключений. Контракт ошибок API подробно разобран в статье про ошибки Node.js API.

Guards: можно ли выполнить запрос

Guard реализует canActivate и возвращает true или false (можно промис). Он видит ExecutionContext - значит, может прочитать метаданные обработчика, например требуемую роль. Ниже минимальный пример проверки ключа API; в реальном проекте сравнивайте секреты за постоянное время и храните их вне кода.

TypeScript// Guard решает, пускать ли запрос дальше import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' @Injectable() export class ApiKeyGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest<{ headers: Record<string, string | undefined> }>() const expected = process.env.API_KEY return Boolean(expected) && request.headers['x-api-key'] === expected } }

Порядок обработки запроса

Этот вопрос задают почти всегда. Запомните последовательность из документации Nest:

  1. Middleware - глобальные, затем модульные. Работают до определения обработчика.
  2. Guards - глобальные, контроллера, метода. Могут остановить запрос.
  3. Interceptors до вызова - логирование, таймауты, кеш.
  4. Pipes - преобразование и валидация параметров.
  5. Обработчик контроллера и сервисы.
  6. Interceptors после вызова - преобразование ответа (в обратном порядке).
  7. Фильтры исключений - если на любом шаге было выброшено исключение.

Сильный ответ добавляет смысл: guard - авторизация, pipe - данные, interceptor - сквозная логика вокруг вызова, фильтр - единый формат ошибок.

Interceptor и фильтр исключений

Interceptor оборачивает вызов обработчика: код до next.handle() выполняется перед ним, а операторы RxJS после - над результатом. Так удобно замерять время, добавлять заголовки или приводить ответ к единому формату. Фильтр исключений, наоборот, срабатывает, когда что-то пошло не так, и превращает исключение в ответ.

JavaScript// Interceptor: замер времени обработки без изменения контроллеров import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common' import { Observable, tap } from 'rxjs' @Injectable() export class TimingInterceptor implements NestInterceptor { private readonly logger = new Logger('Timing') intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { const started = Date.now() const name = context.getClass().name + '.' + context.getHandler().name return next.handle().pipe(tap(() => this.logger.log(name + ': ' + (Date.now() - started) + ' мс'))) } }

Подключается он так же, как guard: декоратором @UseInterceptors на контроллере или методе либо глобально через app.useGlobalInterceptors. Если интервьюер спросит, почему не middleware, ответ простой: middleware не знает, какой обработчик будет вызван, и не видит результат, который тот вернул.

Что ещё спрашивают

  • Циклические зависимости модулей: forwardRef помогает, но чаще это сигнал вынести общий код в отдельный модуль.
  • Конфигурация: ConfigModule и проверка переменных окружения при старте, а не при первом запросе.
  • Тестирование: Test.createTestingModule и подмена провайдеров через overrideProvider.
  • Микросервисы: транспорты Nest для брокеров сообщений (Kafka, RabbitMQ, NATS) и отличие паттернов запрос - ответ и событие.

Как подготовиться

Соберите маленький сервис: модуль пользователей, guard, ValidationPipe, свой interceptor с замером времени и фильтр, который возвращает единый формат ошибки. Напишите e2e-тест на 400, 401 и 404. Это закроет большую часть вопросов. Полная программа - на странице менторства Backend на Node.js, темы и роадмап - в статье что должен знать backend-разработчик.

Источники

Документация NestJS: Modules, Providers, Injection scopes, Guards, Pipes, Request lifecycle.

Вопросы по теме

Чем guard отличается от middleware?
Middleware работает до маршрутизации Nest и не знает, какой обработчик будет вызван. Guard получает ExecutionContext и может принять решение с учётом конкретного контроллера, метода и метаданных, например ролей.
Когда нужен scope REQUEST?
Когда провайдеру нужны данные конкретного запроса, например контекст арендатора. Но такой провайдер создаётся на каждый запрос и делает request-scoped всю цепочку зависимостей, поэтому используйте его осознанно.
Где валидировать входные данные?
Обычно в pipe: ValidationPipe с DTO и class-validator или собственный pipe со схемой. Тогда обработчик получает уже проверенные и преобразованные данные.

Читайте также

Все статьи

Обсудим вашу подготовку?

Напишите, что уже умеете и с чем возникли трудности. Если есть проект, пришлите ссылку на него.

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

Что не получается, что уже пробовали. От 100 знаков, сейчас 0.
Где работали или учились, сколько лет, какой стек, сколько откликов и собеседований. От 100 знаков, сейчас 0.
PDF или DOCX, до 5 МБ. Файл видит только администратор.
GitHub, hh, LinkedIn, сайт. Можно несколько, через пробел или с новой строки.