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:
- Middleware - глобальные, затем модульные. Работают до определения обработчика.
- Guards - глобальные, контроллера, метода. Могут остановить запрос.
- Interceptors до вызова - логирование, таймауты, кеш.
- Pipes - преобразование и валидация параметров.
- Обработчик контроллера и сервисы.
- Interceptors после вызова - преобразование ответа (в обратном порядке).
- Фильтры исключений - если на любом шаге было выброшено исключение.
Сильный ответ добавляет смысл: 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.