NestJS
Экспресс-шпаргалка 20/20
- Nest строится вокруг модулей, контроллеров и провайдеров, что задает четкие bounded-context границы.
- DI-контейнер управляет lifecycle провайдеров и снижает связность через constructor injection и токены.
- Контроллеры только принимают/возвращают transport-данные, бизнес-логика должна жить в service/use-case слое.
- DTO +
ValidationPipeформируют входной контракт и отсекают некорректный payload до бизнес-логики. - Pipes валидируют/трансформируют, Guards авторизуют, Interceptors оборачивают flow, Filters нормализуют ошибки.
- Глобальный exception filter должен отдавать единый error contract с code/message/context/traceId.
- JWT+RBAC реализуют через strategy + guards + policy checks, а не только через одну role-проверку.
- Версионирование API делают URL/header/media-type подходом с deprecation policy и backward совместимостью.
- Конфиг в Nest делают через
ConfigModuleи схему валидации env (fail-fast при старте). - Слои
domain/app/infrastructureпозволяют менять transport/DB без переписывания доменной логики. - Работа с БД должна держать транзакционные границы в сервисе и не протекать в контроллеры.
- Тестирование в Nest: unit для логики, integration для модулей, e2e для критических API-потоков.
- Кэширование включают точечно на read-heavy эндпоинтах с TTL и явной инвалидацией.
- Observability: структурные логи, метрики latency/error-rate, трассировка и readiness probes.
- Очереди (BullMQ) выносят долгие задачи из request path и требуют idempotency + retry policy.
- Graceful shutdown обязан закрыть HTTP, DB и воркеры до завершения процесса.
- Circular dependencies лечат рефакторингом границ модулей,
forwardRefиспользуют как временный компромисс. - Microservices transport оправдан только при реальной сервисной границе и независимом масштабировании.
- Error contract для клиентов должен быть стабильным и одинаковым между всеми endpoint.
- Миграция Express -> Nest делается инкрементально по модулям с контрактной совместимостью и shadow-этапами.
1. Как устроена модульная архитектура в NestJS?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Модуль в NestJS задает границу фичи или технического слоя: какие контроллеры принимают HTTP-запросы, какие провайдеры доступны внутри и что модуль экспортирует наружу.
Что сказать на интервью (30-60 секунд)
- Я смотрю на модуль как на публичный API части приложения:
importsпоказывает зависимости,providers- внутреннюю реализацию,controllers- входные точки,exports- то, чем можно пользоваться снаружи. - Хорошая модульная структура не повторяет таблицы базы данных один в один. Например,
OrdersModuleможет зависеть отPaymentsModule, но не должен напрямую лезть в приватные репозитории платежей. - На ревью я проверяю, нет ли "god module", циклических импортов и случайного экспорта всего подряд. Если модуль экспортирует слишком много, граница фактически исчезла.
Пояснение
В NestJS модуль - это класс с декоратором @Module(). В метаданных модуля описывают контроллеры, провайдеры, импортируемые модули и экспортируемые провайдеры. Важная мысль для интервью: модуль не просто папка. Он управляет видимостью зависимостей.
По умолчанию провайдеры инкапсулированы внутри модуля. Другой модуль не должен получать их напрямую, пока они не экспортированы. Поэтому exports - это публичный контракт модуля. Если экспортировать все сервисы "на всякий случай", приложение быстро превращается в связанную сеть, где любое изменение ломает соседей.
Мини-пример
@Module({
imports: [PaymentsModule],
controllers: [OrdersController],
providers: [CreateOrderUseCase, OrdersRepository],
exports: [CreateOrderUseCase],
})
export class OrdersModule {}
Углубление (2-3 минуты)
Представим сервис заказов. Пользователь создает заказ, сервис проверяет корзину, резервирует товар и запускает оплату. Удобно выделить OrdersModule, InventoryModule и PaymentsModule, но важно договориться, что именно они экспортируют.
Если OrdersModule импортирует PaymentsModule, ему обычно нужен не весь платежный слой, а один публичный use-case или порт, например StartPaymentPort. Так у команды появляется понятная граница: заказ не знает детали платежного провайдера, а платежи не тянут внутрь бизнес-логику заказов.
Trade-off: слишком мелкие модули создают лишнюю навигацию и boilerplate, слишком крупные скрывают связи и усложняют тестирование. Для Middle/Senior ответа важно показать, что вы выбираете границы по бизнес-потоку и изменяемости, а не только по названию сущностей.
Практика
Составьте карту границ для одного backend-флоу:
| Модуль | Зачем существует | Что импортирует | Что экспортирует |
|---|---|---|---|
OrdersModule | создание и просмотр заказов | PaymentsModule, InventoryModule | CreateOrderUseCase |
PaymentsModule | запуск и проверка платежей | HttpModule, ConfigModule | StartPaymentPort |
InventoryModule | резервирование товара | DatabaseModule | ReserveStockUseCase |
После этого отметьте красным все экспорты, которые нужны только "чтобы достучаться". Это кандидаты на пересмотр границ.
Типичные ошибки
- Делают один
AppModule, куда складывают все контроллеры и сервисы. - Экспортируют каждый provider, потому что так проще решить DI-ошибку.
- Делят модули только по таблицам:
UsersModule,OrdersModule,PaymentsModule, но не продумывают реальные бизнес-потоки. - Используют
GlobalModuleдля всего общего и теряют явные зависимости.
Follow-up вопросы
- Когда вы бы разделили
OrdersModuleна несколько модулей? - Чем плох экспорт всех сервисов из shared-модуля?
- Как обнаружить circular dependency между модулями до production?
Что повторить
@Module():imports,controllers,providers,exports.- Инкапсуляцию providers внутри NestJS-модулей.
- Разницу между feature module, shared module и global module.
- Пройдите связанные вопросы 2, 3 и 17 из этого блока.
Связанные модули и карта
2. Что такое providers и как работает DI в Nest?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Provider - это класс или значение, которым управляет DI-контейнер Nest. Обычно это service, repository, factory, adapter или use-case, который внедряется через constructor injection.
Что сказать на интервью (30-60 секунд)
- В NestJS я стараюсь зависеть от явных providers, а не создавать зависимости руками через
new. Тогда контейнер управляет жизненным циклом и тесты могут подменять реализации. - Простая схема: controller получает use-case/service, service получает repository или external adapter, adapter получает config/http client.
- Если зависимость нестабильная или внешняя, я прячу ее за interface/token и подменяю реализацию в тестах или в другом окружении.
Пояснение
DI в Nest помогает не связывать классы жестко. Вместо того чтобы controller сам создавал UsersService, он объявляет зависимость в конструкторе, а Nest на старте приложения строит граф providers и внедряет нужные экземпляры.
Provider по умолчанию живет в scope приложения. Request-scoped providers есть, но их не стоит включать без причины: они дороже, потому что создаются чаще и могут усложнить граф зависимостей. Для большинства сервисов достаточно singleton-поведения, если они не хранят mutable per-request state.
Мини-пример
@Injectable()
export class CreateUserUseCase {
constructor(private readonly usersRepository: UsersRepository) {}
async execute(input: CreateUserDto) {
return this.usersRepository.create(input);
}
}
Углубление (2-3 минуты)
Представим интеграцию с платежным провайдером. Плохой вариант: OrdersService напрямую импортирует SDK провайдера, читает env-переменные и сам делает HTTP retry. В таком коде сложно тестировать заказ без реальной оплаты.
Лучший вариант: OrdersService зависит от PaymentClient или StartPaymentPort, а конкретная реализация живет в PaymentsModule. В тесте можно заменить provider на fake. В production можно заменить реализацию провайдера, не меняя бизнес-логику заказов.
На интервью полезно проговорить граф зависимостей: OrdersController -> CreateOrderUseCase -> OrdersRepository + StartPaymentPort. Так интервьюер видит, что вы понимаете не только синтаксис @Injectable(), но и архитектурный смысл DI.
Практика
Нарисуйте provider graph для фичи create order:
OrdersController
-> CreateOrderUseCase
-> OrdersRepository
-> ReserveStockUseCase
-> StartPaymentPort
Затем ответьте: какую зависимость вы будете мокать в unit-тесте use-case, а какую проверять в integration-тесте модуля?
Типичные ошибки
- Создают сервисы через
new, обходя DI-контейнер. - Прячут внешние клиенты внутри бизнес-сервисов без отдельного adapter/provider.
- Используют request scope как универсальное решение вместо явной передачи request context.
- Лечат DI-ошибки случайными экспортами из модулей, не понимая границу видимости.
Follow-up вопросы
- Когда нужен custom provider token?
- Чем request-scoped provider отличается от singleton provider?
- Как подменить provider в тестах Nest-модуля?
Что повторить
@Injectable(), constructor injection и provider registration.useClass,useValue,useFactory,useExisting.- Provider scope: singleton, request, transient.
- Testing module overrides в NestJS.
Связанные модули и карта
3. Где должна жить бизнес-логика: controller или service?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Controller должен быть тонким HTTP-адаптером, а бизнес-логика должна жить в service/use-case слое. Controller принимает request, валидирует/мапит вход и вызывает сценарий.
Что сказать на интервью (30-60 секунд)
- Я не кладу бизнес-правила в controller, потому что controller привязан к transport-слою: HTTP params, headers, decorators, status codes.
- В controller оставляю маршрутизацию, auth metadata, DTO и маппинг ответа. Правила вроде "нельзя отменить оплаченный заказ" живут в use-case/service.
- Проверяю границу тестами: бизнес-сценарий должен тестироваться без HTTP, а controller - отдельным тестом на route/status/shape response.
Пояснение
Контроллер в Nest отвечает за обработку входящего request и возврат response. Это не значит, что в нем должна жить вся логика endpoint. Если controller начинает проверять статусы заказа, ходить в несколько репозиториев и решать, когда отправить уведомление, он становится трудно тестируемым и плохо переиспользуемым.
Сервис или use-case лучше подходит для бизнес-правил, потому что его можно вызвать из HTTP controller, queue consumer, cron job или другого application flow. Это особенно важно в проектах, где один и тот же сценарий позже появляется в REST API, админке и фоновой обработке.
Мини-пример
@Post()
create(@Body() dto: CreateOrderDto) {
return this.createOrderUseCase.execute(dto);
}
@Injectable()
export class CreateOrderUseCase {
async execute(dto: CreateOrderDto) {
// business rules live here, not in the controller
}
}
Углубление (2-3 минуты)
Реальный кейс: endpoint POST /orders/:id/cancel. Controller может достать id из params и userId из auth context, но правило отмены должно быть в use-case: заказ существует, принадлежит пользователю, еще не оплачен, складской резерв можно снять, событие отмены нужно опубликовать.
Если это лежит в controller, следующий канал - например queue-команда на автоматическую отмену просроченных заказов - либо начнет дублировать правила, либо будет искусственно дергать HTTP-слой. Если правила лежат в use-case, новый transport просто вызывает тот же сценарий.
Trade-off: слишком абстрактный service слой может стать "папкой для всего". Поэтому полезно называть сервисы по сценариям (CancelOrderUseCase), а не только по сущностям (OrdersService), когда логика становится сложной.
Практика
Разложите endpoint по ответственности:
| Ответственность | Где держать |
|---|---|
@Post, @Param, @Body, HTTP status | controller |
| Проверка DTO shape | pipe / validation |
| "Можно ли отменить заказ" | use-case/service |
| SQL/ORM запросы | repository |
| Вызов платежного провайдера | adapter/provider |
| Формат публичного ответа | presenter/mapper или controller |
Типичные ошибки
- Держат в controller бизнес-ветвления на десятки строк.
- Дублируют одно правило в HTTP endpoint, cron job и queue consumer.
- Делают "service" тонкой прокладкой, а всю работу оставляют в controller.
- Возвращают ORM entity напрямую как public response.
Follow-up вопросы
- Когда достаточно обычного service, а когда лучше выделить use-case class?
- Где вы будете мапить domain error в HTTP status?
- Как протестировать бизнес-правило без поднятия HTTP сервера?
Что повторить
- Controllers как request/response слой.
- Application service / use-case pattern.
- Repository и external adapter boundaries.
- Отличие unit-теста use-case от e2e-теста controller.
Связанные модули и карта
4. Как реализовать DTO и валидацию входящих данных?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
DTO описывает shape входных данных, а ValidationPipe проверяет и при необходимости трансформирует payload до попадания в handler. Это первая линия защиты API-контракта.
Что сказать на интервью (30-60 секунд)
- Для входного body/query/params я использую DTO-классы с
class-validatorи включаюValidationPipe. - В production обычно включаю
whitelist, чтобы отбрасывать лишние поля, и обсуждаюforbidNonWhitelisted, если API должен жестко отклонять неожиданный input. - Важно разделять DTO и domain model: DTO описывает транспортный контракт, а не внутреннюю сущность базы данных.
Пояснение
NestJS pipes выполняются перед route handler и подходят для validation/transformation. ValidationPipe работает с DTO-классами и декораторами валидатора. Типичный пример: email должен быть email, age - числом, id в params - UUID или integer.
Сильный ответ на интервью не ограничивается фразой "ставим @IsString()". Нужно объяснить, что валидация защищает границу системы: controller не должен получать payload с неожиданными полями, неверными типами и пропущенными обязательными значениями.
В этом проекте нет подтвержденного runtime REST/error contract в .agent-workflow/contracts, поэтому в контенте нельзя утверждать конкретную форму error response для приложения. Можно говорить только о принципе: ошибочный input должен давать предсказуемый client-facing validation error согласно контракту конкретного проекта.
Мини-пример
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@MinLength(2)
name!: string;
}
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
Углубление (2-3 минуты)
Реальный кейс: endpoint создания пользователя принимает email, name и optional referralCode. Если не включить whitelist/строгую проверку, клиент может прислать role: "admin" или поле, которое случайно попадет дальше в ORM create. Даже если сейчас это не уязвимость, такая граница становится опасной при будущих изменениях.
transform: true полезен для params/query, потому что HTTP приносит строки. Но его нельзя воспринимать как магию: нужно явно проверять типы, диапазоны, enum-значения и формат идентификаторов.
Trade-off: слишком строгая валидация может ломать старых клиентов при добавлении полей, слишком мягкая - пропускать мусор в систему. Поэтому правила DTO должны совпадать с публичным API-контрактом и версионированием.
Практика
Перед интервью прогоните DTO checklist:
| Проверка | Вопрос |
|---|---|
| Required/optional | Какие поля обязательны, а какие можно не присылать? |
| Extra fields | Что будет с неизвестными полями? |
| Type conversion | Где строка из query превращается в number/boolean? |
| Nested DTO | Валидируются ли вложенные объекты и массивы? |
| Error shape | Соответствует ли validation error контракту проекта? |
Типичные ошибки
- Используют TypeScript interface вместо DTO class и ждут runtime-валидации.
- Не включают
whitelist, из-за чего лишние поля проходят дальше. - Смешивают DTO, ORM entity и domain model в один класс.
- Валидируют body, но забывают params/query.
Follow-up вопросы
- Зачем DTO должен быть class, а не interface?
- Что делает
transformвValidationPipe? - Как валидировать массив вложенных объектов?
Что повторить
ValidationPipe,class-validator,class-transformer.whitelist,forbidNonWhitelisted,transform.- DTO для body, query и params.
- Разницу между validation error и domain error.
Связанные модули и карта
5. Когда использовать pipes, guards, interceptors и filters?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Pipes валидируют и трансформируют входные аргументы, guards решают, можно ли выполнять handler, interceptors оборачивают выполнение до/после handler, filters управляют обработкой исключений.
Что сказать на интервью (30-60 секунд)
- Я выбираю инструмент по месту в lifecycle: guard - доступ до handler, pipe - корректность input, interceptor - cross-cutting логика вокруг вызова, filter - перевод исключения в response.
- Не кладу authorization в pipe и не превращаю interceptor в место для бизнес-правил. Иначе код становится непредсказуемым.
- На интервью полезно проговорить порядок: middleware, guards, interceptors, pipes, controller/service, затем response path и exception filters.
Пояснение
Эти механизмы похожи тем, что стоят вокруг controller, но решают разные задачи. Pipe отвечает за аргументы handler: например, ParseIntPipe превращает id в number или выбрасывает ошибку, а ValidationPipe проверяет DTO.
Guard принимает решение, будет ли request обработан. Это место для auth, roles, permissions, feature access. Interceptor удобен для логирования, метрик, timeout, response mapping, cache wrapping или tracing. Exception filter нужен, когда нужно контролируемо обработать исключение и сформировать response по контракту проекта.
Ключевая мысль: не надо выбирать механизм по вкусу. Надо смотреть, на каком этапе lifecycle должна сработать логика и какие данные ей нужны.
Мини-пример
@UseGuards(AuthGuard, RolesGuard)
@Get('admin')
getAdminData(@Query('limit', ParseIntPipe) limit: number) {
return this.adminDashboardService.getSummary(limit);
}
Углубление (2-3 минуты)
Реальный endpoint: GET /admin/reports?limit=20. Сначала guard проверяет, что пользователь аутентифицирован и имеет право смотреть отчеты. Затем pipe проверяет limit и превращает его в number. Interceptor может добавить tracing/metrics или привести успешный ответ к единому формату. Если в handler или ниже возникает исключение, filter переводит его в client-facing error.
Если перепутать роли, появляются странные эффекты. Например, pipe не должен решать, имеет ли пользователь роль admin: он работает с input argument, а не с policy. Filter не должен исправлять бизнес-ошибки молча: он должен представить ошибку в понятном формате, сохранив семантику.
Для Senior-уровня важно назвать не только инструменты, но и границы: что будет global, что controller-level, что route-level, и почему.
Практика
Матрица выбора:
| Задача | Инструмент | Почему |
|---|---|---|
| Проверить JWT/роль | guard | решает, можно ли выполнять handler |
| Проверить body/query/param | pipe | работает с input arguments до handler |
| Добавить tracing/metrics | interceptor | оборачивает execution flow |
| Преобразовать successful response | interceptor | видит результат handler |
| Сформировать validation/domain error response | filter | контролирует exception flow |
Типичные ошибки
- Проверяют права доступа в pipe.
- Пишут бизнес-логику в interceptor, потому что он "удобно оборачивает".
- Делают один global filter, который скрывает разные типы ошибок под одинаковым ответом.
- Не понимают порядок lifecycle и удивляются, почему код не выполняется.
Follow-up вопросы
- В каком порядке выполняются guards, interceptors и pipes?
- Почему authorization лучше держать в guard?
- Когда interceptor лучше middleware?
Что повторить
- Request lifecycle NestJS.
- Built-in pipes:
ParseIntPipe,ParseUUIDPipe,ValidationPipe. CanActivate,ExecutionContext,NestInterceptor,ExceptionFilter.- Разницу между auth error, validation error и domain error.
Связанные модули и карта
6. Как организовать глобальную обработку ошибок в NestJS?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
В NestJS базовая обработка ошибок уже есть, а ExceptionFilter нужен, когда проекту требуется контролируемо мапить исключения в HTTP-ответ, логирование и контракт ошибок.
Что сказать на интервью (30-60 секунд)
- Я разделяю domain error, validation error, auth error и unexpected error. Они не должны превращаться в одинаковый
500. - В Nest можно использовать встроенные
HttpExceptionи стандартные исключения, а для единого поведения добавить globalExceptionFilter. - В filter я не чиню бизнес-логику. Он только переводит уже возникшую ошибку в понятный client-facing response и логирует технический контекст без утечки секретов.
- В этом проекте нет зафиксированного runtime error envelope, поэтому я бы сначала согласовал контракт, а уже потом утверждал точные поля ответа.
Пояснение
NestJS имеет встроенный exception layer: если ошибка не обработана кодом приложения, Nest перехватывает ее и формирует HTTP-ответ. Для HttpException и наследников Nest знает status code и response body. Для неизвестных исключений безопаснее возвращать generic internal error, а детали оставлять в логах.
ExceptionFilter дает контроль над тем, как исключения превращаются в response. Например, можно добавить path, requestId, error code из доменной ошибки или привести validation errors к контракту API. Но это должно соответствовать договоренности проекта, а не вкусу разработчика в одном endpoint.
Сильный ответ на интервью: "Я не бросаю сырые ORM/SDK errors наружу. Я маплю их в доменные или HTTP-ошибки на границе слоя, а global filter отвечает за стабильный формат ответа и логирование".
Мини-пример
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
path: request.url,
message: exception.message,
});
}
}
Углубление (2-3 минуты)
Реальный кейс: пользователь создает заказ, но платежный провайдер недоступен. Плохой вариант - наружу улетает сырой stack trace или generic 500 без понятного кода. Хороший вариант - application layer бросает доменную ошибку вроде PaymentTemporarilyUnavailable, а HTTP-слой мапит ее в согласованный status и безопасное сообщение.
Важно не превращать filter в место, где решают бизнес-ветвления. Если заказ нельзя отменить после оплаты, это правило должно быть в use-case. Filter только решает, как уже возникшую ошибку представить клиенту и как залогировать для поддержки.
Trade-off: слишком подробный error response помогает клиенту, но может раскрыть внутренние детали. Слишком общий response безопаснее, но ухудшает диагностику. Поэтому нужен контракт: какие поля публичные, что уходит только в logs/tracing, какие ошибки retryable.
Практика
Составьте error mapping table для одного endpoint:
| Сценарий | Где возникает | HTTP смысл | Что нельзя отдавать клиенту |
|---|---|---|---|
| Невалидный DTO | pipe | client input error | stack trace |
| Нет токена | guard | unauthenticated | детали проверки подписи |
| Нет роли | guard/policy | forbidden | список внутренних ролей |
| Заказ уже оплачен | use-case | domain conflict | ORM entity |
| Провайдер недоступен | adapter/use-case | temporary failure | raw provider response |
Типичные ошибки
- Отдают наружу raw
error.messageот базы, SDK или внешнего API. - Все ошибки мапят в
500, включая validation/auth/domain conflicts. - Делают global filter, который скрывает разные ошибки под одинаковым ответом.
- Утверждают конкретный error envelope без OpenAPI/contract/source of truth.
Follow-up вопросы
- Чем
UnauthorizedExceptionотличается отForbiddenException? - Где мапить domain error в HTTP status: в use-case или adapter/controller layer?
- Что должно попасть в публичный response, а что только в logs?
Что повторить
HttpException, built-in HTTP exceptions иExceptionFilter.- Разницу между validation, auth, domain и unexpected errors.
- Error contract: status, code, message, requestId, retryability.
- Связанные вопросы 4, 5 и 19 из этого NestJS-блока.
Связанные модули и карта
7. Как реализовать JWT auth и role-based access (RBAC)?
Теги: nestjs, backend, typescript, security, auth
Сложность: Middle/Senior
Короткий ответ
JWT auth обычно проверяет личность пользователя, а RBAC проверяет право выполнить действие. В NestJS это удобно разделять через strategy/guard для AuthN и отдельный guard/policy layer для AuthZ.
Что сказать на интервью (30-60 секунд)
- Я разделяю authentication и authorization: JWT отвечает "кто пользователь", RBAC/policy отвечает "что ему можно".
- В Nest guard хорошо подходит для проверки доступа до handler. Роли можно хранить в metadata через decorator, а guard сравнивает metadata с user context.
- JWT не решает все сам: нужны сроки жизни, refresh/logout/revocation политика, проверка подписи, issuer/audience и поведение при смене ролей.
- Если контракт проекта не описан, нельзя утверждать конкретные cookies/headers/refresh flow; можно описать только архитектурный подход.
Пояснение
В NestJS authentication часто строят вокруг Passport strategy или собственного guard, который валидирует токен и кладет user context в request. После этого authorization guard может проверить роли, permissions или policy.
RBAC удобен для простых систем: admin, manager, user. Но на реальных проектах часто появляется ABAC/policy logic: пользователь может редактировать только свой ресурс, менеджер - только ресурсы своей команды, админ - все. Поэтому хороший ответ не сводится к @Roles('admin').
Главный риск JWT: токен самодостаточный. Если роль изменилась или аккаунт заблокировали, старый access token может жить до истечения срока. Это решается коротким TTL, refresh policy, server-side session/revocation list или дополнительной проверкой для чувствительных действий.
Мини-пример
@SetMetadata('roles', ['admin'])
@UseGuards(JwtAuthGuard, RolesGuard)
@Get('admin/users')
findAdminUsers() {
return this.usersQuery.getAdminList();
}
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const user = request.user;
return user?.roles?.includes('admin') === true;
}
}
Углубление (2-3 минуты)
Реальный кейс: сотрудника перевели из роли manager в user, но у него еще есть старый access token. Если сервис доверяет только claim roles внутри JWT, доступ может сохраниться до истечения токена. Для обычного просмотра это может быть приемлемо при коротком TTL, а для финансовых операций лучше проверить актуальные permissions или session version.
На интервью стоит проговорить flow: login выдает token, strategy проверяет подпись/expiry/issuer, guard кладет user context, roles/policy guard проверяет действие, controller вызывает use-case. Ошибки доступа должны отличаться: unauthenticated - нет валидной личности, forbidden - личность есть, но прав не хватает.
Trade-off: stateless JWT масштабируется проще, но сложнее мгновенно отзывать. Stateful session или revocation list дают контроль, но добавляют хранилище и latency. Выбор зависит от риска продукта.
Практика
Заполните policy matrix:
| Endpoint | AuthN нужен | Роли | Resource rule | Ошибка без токена | Ошибка без права |
|---|---|---|---|---|---|
GET /profile | да | any user | только свой профиль | unauthenticated | forbidden |
GET /admin/users | да | admin | все пользователи | unauthenticated | forbidden |
PATCH /orders/:id | да | user/admin | владелец или admin | unauthenticated | forbidden |
Типичные ошибки
- Путают
401и403: "не вошел" и "нет права" становятся одним случаем. - Доверяют роли из долгоживущего JWT без политики отзыва или короткого TTL.
- Проверяют роли вручную внутри каждого controller method.
- Хранят секреты JWT в коде или логируют token payload целиком.
Follow-up вопросы
- Чем AuthN отличается от AuthZ?
- Что произойдет, если роль пользователя изменилась после выдачи JWT?
- Когда RBAC уже недостаточно и нужна policy/ABAC модель?
Что повторить
- Guards,
ExecutionContext, custom decorators и metadata. - JWT claims:
sub,exp,iat,iss,aud. 401 Unauthorizedvs403 Forbidden.- Refresh/revocation/session-version trade-offs.
Связанные модули и карта
8. Как строить версионирование API в Nest?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Версионирование API нужно для breaking changes: NestJS поддерживает URI, header, media type и custom versioning, но технический механизм должен сопровождаться compatibility и deprecation policy.
Что сказать на интервью (30-60 секунд)
- Я версионирую не каждое изменение, а breaking changes: удаление поля, изменение смысла поля, новый обязательный input, несовместимый формат ответа.
- В Nest можно включить
app.enableVersioning()и выбрать URI/header/media type. Но главное - как поддерживать старых клиентов и когда выключать старую версию. - Additive changes лучше делать без новой версии: добавили optional поле или новый endpoint - старые клиенты не ломаются.
- Перед изменением контракта я проверяю OpenAPI/клиентов/consumers; в этом проекте таких runtime contracts нет, значит нельзя утверждать конкретную стратегию API.
Пояснение
NestJS позволяет держать разные версии controller или route внутри одного приложения. Например, v1 возвращает старый shape ответа, а v2 - новый. Это полезно, когда клиенты не могут обновиться одновременно.
Но версионирование не должно быть заменой аккуратному контракту. Если каждое поле ведет к новой версии, поддержка быстро становится дорогой. Если breaking changes выкатываются без версии, клиенты ломаются внезапно. Хорошая стратегия описывает тип versioning, срок поддержки, deprecation headers/docs, migration guide и contract tests.
На интервью сильный сигнал - вы умеете отличать v2 нужен от можно сделать backward-compatible.
Мини-пример
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI,
});
@Controller({
path: 'users',
version: '2',
})
export class UsersV2Controller {}
Углубление (2-3 минуты)
Реальный кейс: mobile app v1 ожидает fullName, а новый backend хочет отдавать { firstName, lastName }. Если заменить поле на месте, старые клиенты покажут пустые имена. Безопаснее оставить v1, добавить v2, объявить deprecation window и измерять трафик старой версии.
Для backend-to-backend интеграций header/media type versioning может быть удобнее URI, потому что URL остается стабильным, а контракт выбирается через header. Для публичного простого REST часто проще URI versioning: /v1/users, /v2/users.
Trade-off: несколько версий увеличивают поддержку и тестовую матрицу. Поэтому нужно планировать удаление старой версии и не держать ее бесконечно.
Практика
Составьте compatibility plan:
| Изменение | Breaking? | Нужна версия? | Migration action |
|---|---|---|---|
Добавить optional поле avatarUrl | нет | нет | обновить docs/tests |
Переименовать fullName в name | да | да | оставить v1, добавить v2 |
Сделать phone обязательным | да | да/feature rollout | миграция клиентов |
| Добавить новый endpoint | нет | нет | описать в контракте |
Типичные ошибки
- Называют любое изменение
v2, хотя оно backward-compatible. - Делают breaking change без периода совместимости.
- Дублируют всю бизнес-логику между
V1ControllerиV2Controller. - Не измеряют, кто еще ходит в старую версию.
Follow-up вопросы
- Когда выбрать URI versioning, а когда header versioning?
- Как отличить breaking change от additive change?
- Как не задублировать бизнес-логику между версиями?
Что повторить
app.enableVersioning()иVersioningType.- Controller-level и route-level versions.
- Backward compatibility, deprecation window, contract tests.
- Связанные вопросы 4, 6 и 19.
Связанные модули и карта
9. Как организовать конфигурацию приложения и env?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Конфигурацию в NestJS обычно выносят в ConfigModule, валидируют env на старте и отдают остальному коду через typed config или ConfigService, а не через хаотичный process.env по всему проекту.
Что сказать на интервью (30-60 секунд)
- Я предпочитаю fail-fast: если нет
DATABASE_URL, JWT secret или неверныйPORT, приложение не должно стартовать в полурабочем состоянии. - Config должен быть централизован и типизирован: бизнес-сервис не должен сам читать
process.envи гадать, есть ли значение. - Секреты не коммитятся в репозиторий, не логируются и не попадают в client bundle. Для Docusaurus/runtime frontend это особенно важно разделять.
- Для тестов и staging нужен явный набор env, иначе баги появляются только при деплое.
Пояснение
@nestjs/config дает ConfigModule и ConfigService, чтобы читать env и конфигурационные файлы централизованно. Официальный подход поддерживает schema validation: приложение может выбросить ошибку на старте, если обязательная переменная отсутствует или не проходит правила.
Плохой признак - process.env разбросан по controllers, services и adapters. Тогда невозможно быстро понять, какие env нужны приложению, какие значения optional, где default, а где секрет. Лучше собрать config per domain: database, auth, payments, http.
Для интервью важно показать lifecycle: load env -> validate -> build typed config -> inject into providers -> fail before serving traffic.
Мини-пример
ConfigModule.forRoot({
isGlobal: true,
validate(config) {
if (!config.DATABASE_URL) {
throw new Error('DATABASE_URL is required');
}
return config;
},
});
@Injectable()
export class DatabaseConfig {
constructor(private readonly config: ConfigService) {}
get url() {
return this.config.getOrThrow<string>('DATABASE_URL');
}
}
Углубление (2-3 минуты)
Реальный кейс: в staging забыли PAYMENT_WEBHOOK_SECRET. Приложение стартует, healthcheck зеленый, но webhook validation падает только при первом платеже. Это плохая конфигурационная граница. Если переменная обязательна для включенного payment module, сервис должен упасть на старте или явно отключить feature.
Еще один кейс: JWT_SECRET случайно отличается между двумя репликами. Пользователь логинится через одну реплику, а следующий request попадает на другую и получает 401. Это не "рандомный auth bug", а проблема конфигурации и rollout.
Trade-off: fail-fast повышает надежность, но требует аккуратной матрицы env для local/test/stage/prod. Слишком мягкие defaults удобны локально, но опасны в production.
Практика
Соберите env/config checklist:
| Config | Required где | Default допустим? | Проверка на старте | Секрет? |
|---|---|---|---|---|
DATABASE_URL | all non-test | нет | да | да |
PORT | all | да | number range | нет |
JWT_SECRET | auth enabled | нет | min length | да |
PAYMENT_WEBHOOK_SECRET | payments enabled | нет | да | да |
LOG_LEVEL | all | да | enum | нет |
Типичные ошибки
- Читают
process.envпрямо внутри business logic. - Дают production-опасные defaults для секретов или внешних URL.
- Не валидируют env на старте и получают runtime-падения после деплоя.
- Логируют полный config вместе с секретами.
Follow-up вопросы
- Что должно случиться, если обязательный env отсутствует?
- Где хранить default values и где они опасны?
- Как проверить, что секрет не попал в public frontend bundle?
Что повторить
ConfigModule.forRoot,ConfigService, validation schema/custom validate.- Per-environment config: local, test, staging, production.
- Secrets handling and redaction.
- Health/readiness для зависимости, которая включена через config.
Связанные модули и карта
10. Как строить слоистую архитектуру (domain/app/infrastructure)?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Слоистая архитектура разделяет business rules, application use-cases и infrastructure adapters так, чтобы домен не зависел от HTTP, ORM, queues и внешних SDK.
Что сказать на интервью (30-60 секунд)
- Я держу controller как transport adapter, use-case как application layer, domain rules отдельно, а database/payment/email - в infrastructure adapters.
- Направление зависимостей должно идти внутрь: infrastructure может знать о domain contracts, но domain не должен импортировать Prisma, TypeORM, HTTP SDK или Nest decorators.
- Это не религия папок. Смысл в том, чтобы бизнес-правила тестировались без базы и HTTP, а инфраструктура заменялась через ports/providers.
- Для простой CRUD-фичи можно не усложнять, но при сложных правилах, интеграциях и очередях слои быстро окупаются.
Пояснение
NestJS сам по себе не навязывает domain/app/infrastructure архитектуру. Он дает modules, providers и DI, на которых можно построить такую структуру. Например, controller вызывает CreateOrderUseCase, use-case зависит от OrdersRepositoryPort и PaymentPort, а конкретные Prisma/HTTP adapters реализуют эти порты.
Главный критерий: можно ли протестировать бизнес-сценарий без поднятия Nest application, базы и внешних API. Если нет, вероятно, логика слишком сильно срослась с infrastructure.
Слишком раннее усложнение тоже вредно. Если endpoint просто читает справочник без правил, полноценный domain layer может быть лишним. Senior-ответ должен показывать баланс: слои вводятся там, где есть изменяемость, правила и дорогие зависимости.
Мини-пример
export interface OrdersRepositoryPort {
save(order: Order): Promise<void>;
}
@Injectable()
export class CreateOrderUseCase {
constructor(private readonly ordersRepository: OrdersRepositoryPort) {}
async execute(input: CreateOrderInput) {
const order = Order.create(input);
await this.ordersRepository.save(order);
return order.id;
}
}
Углубление (2-3 минуты)
Реальный кейс: команда меняет payment provider. Если OrdersService напрямую вызывает SDK старого провайдера, миграция затрагивает бизнес-логику заказов. Если use-case зависит от PaymentPort, меняется adapter, а сценарий "создать заказ и стартовать оплату" остается стабильным.
Второй кейс: нужно запускать тот же сценарий из REST endpoint и из queue retry. Если правила лежат в controller, их придется дублировать. Если есть application use-case, оба transport adapter вызывают один сценарий.
Trade-off: слои добавляют boilerplate и требуют дисциплины. Но они уменьшают связанность там, где фича живет долго, интеграции меняются, а правила нужно тестировать отдельно.
Практика
Нарисуйте dependency map:
HTTP Controller / Queue Consumer
-> Application UseCase
-> Domain Entity / Domain Service
-> Repository Port
-> Payment Port
Infrastructure:
PrismaOrdersRepository implements Repository Port
HttpPaymentAdapter implements Payment Port
Проверьте стрелки: domain не должен импортировать Nest, Prisma, Express, Redis или SDK провайдера.
Типичные ошибки
- Называют папки
domain/app/infra, но оставляют Prisma и HTTP SDK внутри domain. - Создают абстракции для простого CRUD без реальной сложности.
- Дублируют business rules между controller, queue handler и cron job.
- Возвращают ORM entity наружу как public API response.
Follow-up вопросы
- Когда слоистая архитектура избыточна?
- Что такое port/adapter в контексте Nest providers?
- Как протестировать use-case без базы данных?
Что повторить
- Controller/service/use-case boundaries из вопросов 1-3.
- Custom providers and injection tokens.
- Repository pattern, ports/adapters, domain errors.
- Unit tests for use-cases with fake adapters.
Связанные модули и карта
11. Как работать с базой данных в Nest (Prisma/TypeORM)?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
В NestJS базу обычно подключают через ORM/клиент вроде Prisma или TypeORM, а доступ к данным прячут за repository/data-access слоем, чтобы бизнес-логика не зависела от SQL/ORM деталей.
Что сказать на интервью (30-60 секунд)
- Я не тащу Prisma/TypeORM entity прямо в controller response. Обычно есть repository или adapter, который мапит database shape в domain/application shape.
- Для операций с несколькими изменениями сразу я заранее думаю о транзакции: что должно быть атомарно и что можно вынести в очередь или outbox.
- В production я смотрю на N+1 запросы, индексы, connection pool, миграции и поведение при частичном падении БД.
- В текущем проекте нет runtime DB contract, поэтому примеры ниже - именно interview practice, а не описание LazyOffer backend.
Пояснение
NestJS database-agnostic: можно использовать Prisma, TypeORM, Sequelize, Mongoose, raw driver или query builder. Nest дает DI и modules, а не заставляет выбрать конкретную ORM. Поэтому на интервью важнее не название инструмента, а границы: где живут запросы, как тестируется бизнес-логика, как проходят транзакции и миграции.
Слабый ответ: "Я бы подключил Prisma и вызывал prisma.user.findMany() из сервиса". Сильный ответ: "Я отделю use-case от data-access, чтобы смена ORM, добавление transaction boundary или оптимизация запросов не расползались по controller layer".
Отдельно проговорите, что synchronize: true в TypeORM нельзя воспринимать как production migration strategy: схема должна меняться через контролируемые миграции и rollback-план.
Мини-пример
@Injectable()
export class OrdersRepository {
constructor(private readonly prisma: PrismaService) {}
async findOpenByUser(userId: string) {
return this.prisma.order.findMany({
where: { userId, status: 'OPEN' },
});
}
}
Углубление (2-3 минуты)
Реальный кейс: создание заказа должно записать заказ, позиции заказа и резерв товара. Если эти операции идут отдельно без транзакции, можно получить заказ без резерва или резерв без заказа. Если все засунуть в одну огромную транзакцию, можно получить lock contention и деградацию под нагрузкой.
Нормальный ответ показывает баланс: критичные записи - в transaction boundary, внешние вызовы - не внутри транзакции, фоновые side effects - через outbox/queue после commit. Даже если конкретная реализация на Prisma или TypeORM отличается, архитектурный принцип остается тем же.
Trade-off: repository слой добавляет код, но дает тестируемость и контроль над query shape. Прямой ORM-вызов быстрее писать, но он связывает business layer с конкретной инфраструктурой.
Практика
Составьте repository transaction checklist:
| Операция | Нужна транзакция? | Почему | Что не держать внутри |
|---|---|---|---|
| Создать заказ + позиции | да | атомарность заказа | HTTP вызов платежей |
| Обновить статус заказа | иногда | зависит от конкуренции | долгие вычисления |
| Записать audit log | часто вместе/outbox | расследование инцидента | отправку email |
| Прочитать список заказов | нет | read-only | лишние joins без индексов |
Типичные ошибки
- Возвращают ORM entity напрямую как публичный DTO.
- Держат внешний HTTP call внутри database transaction.
- Не замечают N+1 запросы при списках и relations.
- Используют auto-sync схемы вместо миграций в production.
Follow-up вопросы
- Где должна жить transaction boundary?
- Как обнаружить N+1 запрос?
- Чем repository отличается от application service?
Что повторить
- Prisma/TypeORM integration patterns in NestJS.
- Transactions, migrations, indexes, connection pool.
- DTO/domain/entity mapping.
- Связанные вопросы 3, 10 и 14.
Связанные модули и карта
12. Как тестировать Nest-модули и контроллеры?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Тесты NestJS лучше раскладывать по риску: unit для use-case/provider logic, integration для module wiring и e2e для HTTP behavior, guards, pipes и contract-critical paths.
Что сказать на интервью (30-60 секунд)
- Я не пытаюсь все проверять e2e. Business rules быстрее и надежнее тестировать unit-тестами use-case с fake providers.
- Nest
TestingModuleнужен, когда важно проверить DI wiring, override providers, guards/pipes/interceptors или controller behavior. - E2E оставляю для критичных user/API flows: status, validation, auth, response shape и integration между слоями.
- Если тест часто flaky, я смотрю на time, external services, shared state, database cleanup и очереди.
Пояснение
Официальные testing utilities Nest позволяют собрать module в тесте через Test.createTestingModule, получить providers/controllers из DI container, override providers и поднять INestApplication для e2e через HTTP server. Это полезно, потому что Nest-приложение много делает через DI, metadata и lifecycle.
Сильный ответ на интервью - не "покрываю все 100%", а "выбираю уровень теста по риску". Например, расчет скидки - unit. Controller с ValidationPipe и AuthGuard - integration/e2e. Миграция или DB transaction - integration с тестовой БД.
Тест должен ловить конкретную поломку: неверный status, пропущенная validation, неправильно замоканный provider, поломанный module import/export, нарушение contract shape.
Мини-пример
const moduleRef = await Test.createTestingModule({
controllers: [OrdersController],
providers: [
CreateOrderUseCase,
{ provide: OrdersRepository, useValue: fakeOrdersRepository },
],
}).compile();
const controller = moduleRef.get(OrdersController);
Углубление (2-3 минуты)
Реальный кейс: endpoint создания заказа должен отклонить невалидный DTO, проверить auth, создать order и вернуть публичный response. Один огромный e2e-тест проверит все, но будет медленным и хрупким. Лучше разложить: use-case unit проверяет business rules, controller/e2e проверяет validation/auth/status/shape, repository integration проверяет transaction и DB mapping.
Если bug был в module wiring - например забыли экспортировать provider - unit-тест use-case не поймает это. Нужен тест, который собирает настоящий module. Если bug был в business rule, e2e через HTTP может быть избыточным и труднее диагностируемым.
Trade-off: чем выше тест, тем больше уверенности в интеграции, но выше цена и хрупкость. Чем ниже тест, тем быстрее feedback, но меньше гарантий wiring/contract.
Практика
Составьте testing pyramid plan:
| Риск | Уровень теста | Что мокать | Что проверять |
|---|---|---|---|
| Business rule | unit | repository/payment port | result/domain error |
| Module wiring | integration | external SDK | provider available |
| DTO validation | e2e/controller | use-case if нужно | status + validation error |
| DB transaction | integration | external API | rollback/commit |
| Auth guard | e2e/integration | user context/token verify | 401/403 behavior |
Типичные ошибки
- Тестируют business rules только через HTTP e2e.
- Мокают все подряд и фактически не проверяют Nest module wiring.
- Не чистят shared DB/queue state между тестами.
- Проверяют happy path, но пропускают validation/auth/error branches.
Follow-up вопросы
- Когда нужен
overrideProvider? - Чем unit-тест use-case отличается от e2e-теста controller?
- Как бороться с flaky e2e в Nest?
Что повторить
Test.createTestingModule,overrideProvider,createNestApplication.- Unit/integration/e2e boundaries.
- Test data cleanup and deterministic clocks.
- Связанные вопросы 4, 5, 7 и 11.
Связанные модули и карта
13. Как реализовать кеширование и где его лучше включать?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Кеш включают там, где есть дорогие повторяемые чтения, понятный cache key и безопасная стратегия TTL/инвалидации. В NestJS это можно делать через CacheModule, interceptors или отдельный cache provider.
Что сказать на интервью (30-60 секунд)
- Я начинаю с вопроса "что именно кешируем и как инвалидируем", а не с подключения Redis.
- Кеш подходит для read-heavy и expensive data, но опасен для персональных, permission-sensitive и часто меняющихся данных.
- Для Nest можно использовать
CacheInterceptorдля простых GET, но для сложных ключей и invalidation лучше явный cache service/provider. - Проверяю hit rate, stale data risk, memory pressure и поведение при падении cache store.
Пояснение
NestJS поддерживает caching через @nestjs/cache-manager, CacheModule и CacheInterceptor. Это удобно для стандартных response caching сценариев. Но в реальном проекте главная сложность не в подключении модуля, а в корректности cache key и invalidation.
Пример риска: endpoint GET /orders кешируется только по URL, но не учитывает userId или роль. Один пользователь может получить данные другого. Другой риск: после изменения товара старый price остается в кеше и показывается клиенту.
Сильный ответ: "Кеш - это договор о допустимой stale data. Если stale data недопустима, либо не кешируем, либо строим строгую invalidation strategy".
Мини-пример
@UseInterceptors(CacheInterceptor)
@CacheTTL(60)
@Get('catalog')
findCatalog() {
return this.catalogQuery.getPublicCatalog();
}
Углубление (2-3 минуты)
Реальный кейс: главная витрина каталога читает тысячи раз в минуту, а меняется раз в несколько минут. Здесь кеш с TTL может резко снизить нагрузку на БД. Но если кешировать корзину или цены без учета пользователя, региона и скидок, получится security или billing bug.
Кеш можно держать на нескольких уровнях: HTTP/CDN, application cache, query cache, external Redis. Чем ниже уровень, тем больше контроля, но больше кода и ответственности за invalidation.
Trade-off: кеш повышает скорость и снижает нагрузку, но добавляет риск stale data и новые failure modes. Cache down не должен автоматически превращать весь сервис в down, если данные можно безопасно прочитать из source of truth.
Практика
Составьте cache invalidation table:
| Data | Cache key включает | TTL | Invalidation | Риск stale |
|---|---|---|---|---|
| Public catalog | locale, region | 60s | product update event/manual purge | medium |
| User profile | userId | 30s | profile update | privacy risk |
| Permissions | userId, version | very short | role change/session version | security risk |
| Report export | params hash | 10m | no manual, regenerate | stale acceptable |
Типичные ошибки
- Кешируют персональные данные без
userId/permission context в key. - Не имеют стратегии invalidation и надеются только на большой TTL.
- Не измеряют hit rate, поэтому кеш может быть бесполезным.
- Падают всем сервисом при недоступном Redis, хотя можно сходить в source of truth.
Follow-up вопросы
- Как построить cache key для user-specific endpoint?
- Когда
CacheInterceptorнедостаточен? - Что делать, если Redis недоступен?
Что повторить
CacheModule,CacheInterceptor, TTL.- Cache key, invalidation, stale data.
- Redis/application/CDN cache boundaries.
- Связанные вопросы 6, 11 и 14.
Связанные модули и карта
14. Как организовать логирование и observability в Nest?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Observability в Nest - это не просто console.log, а связка структурных логов, метрик, трассировки и correlation/request id, чтобы быстро понять, где и почему деградирует request или background job.
Что сказать на интервью (30-60 секунд)
- Я разделяю logs, metrics и traces: logs объясняют событие, metrics показывают тренд, traces связывают путь request между слоями.
- В Nest можно заменить или расширить logger, добавить interceptor/middleware для request logging, а бизнес-события логировать из use-case/adapters.
- Не логирую токены, пароли, raw payload с персональными данными и секреты. Нужна redaction policy.
- Для интервью важно назвать конкретные сигналы: latency p95/p99, error rate, DB query time, queue lag, cache hit rate.
Пояснение
Nest имеет встроенный logger и возможность подключить custom logger implementation. Но observability шире логгера. Для production важно, чтобы один инцидент можно было проследить от входящего request до DB query, внешнего API, очереди и response.
Структурный лог лучше строки "something failed": он содержит requestId, userId или anonymized subject, route, status, duration, error code, dependency name. При этом sensitive data должна быть вычищена.
Сильный ответ: "Я заранее определяю, какие сигналы нужны для runbook: где ошибка, какая зависимость деградирует, сколько пользователей затронуто, retry помогает или ухудшает".
Мини-пример
@Injectable()
export class OrdersService {
private readonly logger = new Logger(OrdersService.name);
async createOrder(input: CreateOrderInput) {
this.logger.log({ action: 'create_order_started' });
// create order
}
}
Углубление (2-3 минуты)
Реальный кейс: пользователи жалуются, что checkout иногда висит 10 секунд. Без observability это "где-то медленно". С нормальными сигналами видно: route POST /orders, p95 вырос, DB query нормальный, payment API p99 вырос, retry умножает нагрузку, queue lag растет.
В Nest request-level сигналы часто удобно собирать interceptor/middleware, а dependency-level сигналы - в adapters: payment client, repository, queue producer. Ошибки лучше связывать с error filter и correlation id.
Trade-off: слишком мало логов не помогает расследованию, слишком много логов дорого и опасно для privacy. Поэтому нужны уровни логирования, redaction и sampling для шумных событий.
Практика
Составьте observability signal map:
| Flow | Logs | Metrics | Trace/span | Alert |
|---|---|---|---|---|
| HTTP request | route/status/requestId | latency, error rate | controller -> use-case | high 5xx |
| DB query | query name, duration bucket | query p95, pool usage | repository span | pool saturation |
| External API | provider, status, retry | timeout rate | adapter span | provider errors |
| Queue job | jobId, attempt, result | lag, fail rate | processor span | DLQ/fail spike |
Типичные ошибки
- Логируют секреты, токены или полный request body.
- Считают observability набором
console.log. - Не добавляют correlation id, поэтому невозможно связать события одного request.
- Имеют dashboards без alert thresholds и runbook.
Follow-up вопросы
- Какие поля вы добавите в request log?
- Чем metric отличается от log?
- Какие сигналы покажут, что деградирует очередь, а не HTTP handler?
Что повторить
- Nest
Logger, custom logger, request interceptors. - Logs vs metrics vs traces.
- Redaction, request id, alert thresholds.
- Связанные вопросы 6, 13 и 15.
Связанные модули и карта
15. Как реализовать очереди задач (BullMQ) в Nest?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Очереди в NestJS используют, чтобы вынести долгие, повторяемые или retryable задачи из request-response пути. BullMQ дает queue producer, processor/worker, retries, attempts и Redis-backed delivery mechanics.
Что сказать на интервью (30-60 секунд)
- Я выношу в очередь email, webhooks, тяжелые экспорты, image processing, retry внешних API и не критичные side effects после commit.
- Очередь не делает задачу автоматически надежной: нужны idempotency key, retry policy, backoff, dead-letter/manual review и observability.
- В Nest
BullModuleрегистрирует queues/processors, producer добавляет jobs, processor выполняет их отдельно от HTTP request. - В текущем проекте queues/webhooks отсутствуют в contract docs, поэтому это generic preparation content, а не описание LazyOffer runtime.
Пояснение
Очередь полезна, когда HTTP request не должен ждать долгую работу. Например, после создания заказа можно быстро вернуть response, а отправку email и webhook положить в job. Но если job выполнится дважды, результат должен быть безопасным. Поэтому idempotency важнее, чем сам факт использования BullMQ.
Еще один риск - потеря связи между database transaction и job enqueue. Если job добавили до commit, worker может увидеть данные, которых еще нет. Если commit прошел, а job не добавился, side effect потерян. Для критичных событий часто используют outbox pattern.
Сильный ответ: "Я проектирую очередь как отдельный reliability surface: payload schema, idempotency, retries, backoff, monitoring, DLQ/manual recovery".
Мини-пример
@Injectable()
export class EmailProducer {
constructor(@InjectQueue('email') private readonly queue: Queue) {}
async enqueueWelcomeEmail(userId: string) {
await this.queue.add('welcome-email', { userId }, {
jobId: `welcome-email:${userId}`,
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
}
}
Углубление (2-3 минуты)
Реальный кейс: после оплаты нужно отправить чек, письмо и webhook партнеру. Если делать все синхронно в HTTP request, пользователь ждет внешние сервисы, а один timeout ломает checkout. Очередь позволяет вернуть response быстрее и повторить side effects отдельно.
Но очередь добавляет новые failure modes: job может выполниться дважды, зависнуть, упасть после частичного side effect, накопить lag или потерять payload compatibility после деплоя новой версии. Поэтому processor должен быть идемпотентным, а payload - версионированным или совместимым.
Trade-off: очередь разгружает request path и дает retry, но делает систему eventually consistent. Нужно объяснить пользователю/клиенту, что результат появится позже, и иметь мониторинг отставания.
Практика
Заполните queue idempotency/retry matrix:
| Job | Idempotency key | Retry | Backoff | Что делать после fail |
|---|---|---|---|---|
| Welcome email | userId + template | 3 | exponential | manual review if important |
| Payment webhook | provider event id | 5 | exponential | DLQ + alert |
| Report export | report id | 1-2 | fixed | mark failed for user |
| Reindex search | entity id + version | many | delayed | requeue latest version |
Типичные ошибки
- Кладут в job весь ORM entity вместо минимального stable payload.
- Не делают processor идемпотентным и получают дубли email/списаний/webhooks.
- Не мониторят queue lag, failed jobs и retry storms.
- Добавляют job до commit без outbox/компенсации.
Follow-up вопросы
- Когда очередь лучше cron job?
- Что такое idempotency для worker?
- Как связать database transaction и постановку job?
Что повторить
BullModule, queue producer, processor/worker.- Attempts, backoff, concurrency, delayed jobs.
- Outbox, DLQ/manual recovery, queue lag.
- Связанные вопросы 6, 11 и 14.
Связанные модули и карта
16. Как делать graceful shutdown в Nest приложении?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Graceful shutdown в NestJS - это управляемое завершение: перестать принимать новый трафик, дождаться активных операций в пределах timeout, закрыть БД/брокеры/воркеры и не потерять данные.
Что сказать на интервью (30-60 секунд)
- В Nest shutdown hooks нужно включить явно через
app.enableShutdownHooks(), иначе lifecycle hooks на системные сигналы могут не сработать. - Я разделяю stop accepting traffic, drain in-flight requests/jobs, close dependencies и final process exit.
- Для Kubernetes/containers важно, чтобы readiness быстро становился false, а shutdown timeout был согласован с termination grace period.
- Риск не только в HTTP: worker может оборвать job на середине, БД - оставить connection leak, broker - потерять ack.
Пояснение
Nest поддерживает lifecycle hooks: onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown. Они вызываются при app.close() или при системных сигналах, если включены shutdown hooks. Это место, где закрывают database connections, queue workers, message consumers, intervals и внешние клиенты.
Слабый ответ - просто написать process.on('SIGTERM'). В Nest лучше использовать lifecycle hooks, чтобы не обходить DI и не закрывать зависимости хаотично.
Сильный ответ показывает drain strategy: новые requests больше не принимаются, текущие завершаются, jobs либо заканчиваются, либо safely retry, connections закрываются, метрики показывают shutdown duration и interrupted jobs.
Мини-пример
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3000);
}
@Injectable()
export class QueueWorker implements OnApplicationShutdown {
async onApplicationShutdown(signal?: string) {
await this.worker.close();
}
}
Углубление (2-3 минуты)
Реальный кейс: идет deploy, pod получает SIGTERM, но приложение продолжает принимать traffic, а worker обрывает job отправки webhook. Клиент получает timeout, webhook улетает дважды после retry, а в логах нет причины. Это не просто "деплой неудачный", а отсутствие shutdown protocol.
Правильный подход: readiness false, HTTP server перестает принимать новые соединения, in-flight requests получают ограниченное время, workers закрываются после текущей job или переводят ее в retry-safe состояние, DB/broker clients закрываются в конце.
Trade-off: слишком короткий shutdown теряет работу, слишком длинный замедляет deploy и rollback. Поэтому timeout должен быть измеряемым и согласованным с инфраструктурой.
Практика
Составьте shutdown drain checklist:
| Ресурс | Stop accepting | Drain | Timeout | Метрика |
|---|---|---|---|---|
| HTTP | readiness false | in-flight requests | 10-30s | shutdown duration |
| DB | no new queries | wait active transaction | bounded | open connections |
| Queue worker | pause worker | finish current job | job timeout | interrupted jobs |
| Broker consumer | stop consume | ack/nack current message | bounded | unacked messages |
Типичные ошибки
- Не включают
enableShutdownHooks()и думают, что hooks сработают наSIGTERM. - Завершают процесс до закрытия queue workers или DB connections.
- Не имеют timeout и могут зависнуть навсегда.
- Не меняют readiness перед drain и получают новый traffic во время shutdown.
Follow-up вопросы
- Чем
beforeApplicationShutdownотличается отonApplicationShutdown? - Что делать с job, которая не успела завершиться?
- Как проверить graceful shutdown в CI или staging?
Что повторить
- Nest lifecycle hooks and
enableShutdownHooks(). - Kubernetes termination grace period and readiness.
- Queue worker drain, broker ack/nack, DB connection close.
- Связанные вопросы 14 и 15.
Связанные модули и карта
17. Как избегать circular dependencies между модулями?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Circular dependency возникает, когда модули или providers зависят друг от друга по кругу. В NestJS forwardRef() может временно разрулить DI, но правильнее пересобрать границы и направление зависимостей.
Что сказать на интервью (30-60 секунд)
- Я сначала ищу причину цикла: неправильная ownership boundary, shared logic в feature module, взаимные use-cases или barrel imports.
forwardRef()допустим как tactical escape hatch, но если он расползается по проекту, архитектура уже сигналит о проблеме.- Часто цикл лечится выделением shared/domain module, application port, facade или domain event, чтобы зависимость шла в одну сторону.
- Проверяю dependency graph: feature modules не должны импортировать друг друга только ради доступа к приватному service.
Пояснение
Nest docs описывают circular dependencies между providers и modules. forwardRef() позволяет сослаться на provider/module, который еще не определен, а ModuleRef дает другой способ получить provider из DI container. Но это не отменяет того, что круговая зависимость повышает связанность.
Пример: OrdersService вызывает PaymentsService, а PaymentsService вызывает OrdersService, чтобы обновить статус заказа. Лучше сделать PaymentsService независимым adapter/use-case, а обновление заказа вынести в CompletePaymentUseCase или событие PaymentCompleted.
Barrel files (index.ts) тоже могут случайно создавать импортные циклы. На интервью это хороший edge-case: цикл бывает не только в Nest metadata, но и в TypeScript imports.
Мини-пример
Bad:
OrdersModule -> PaymentsModule -> OrdersModule
Better:
OrdersModule -> PaymentsPort
PaymentsInfrastructureModule implements PaymentsPort
PaymentCompleted event -> CompletePaymentUseCase
Углубление (2-3 минуты)
Реальный кейс: заказы импортируют платежи, платежи импортируют заказы, потом добавляется refunds, и каждый новый use-case требует новый forwardRef. Тесты становятся сложнее, module init order - менее очевидным, а изменение платежей неожиданно ломает заказы.
Лучший подход - определить owner потока. Если Orders управляет жизненным циклом заказа, платежный модуль должен отдавать capability "start/check/refund payment", но не дергать внутренности заказов напрямую. Возврат статуса можно сделать через application use-case или event.
Trade-off: выделение ports/events добавляет слой, но снимает двустороннюю связанность. forwardRef быстрее, но оставляет технический долг.
Практика
Составьте dependency graph refactor plan:
| Цикл | Почему возник | Тактический фикс | Долгосрочный фикс |
|---|---|---|---|
Orders <-> Payments | обе фичи меняют order status | forwardRef | PaymentPort + CompletePaymentUseCase |
Users <-> Auth | user lookup смешан с auth flow | facade | UserIdentityPort |
Catalog <-> Search | reindex дергает catalog internals | event | ProductChanged event |
Типичные ошибки
- Ставят
forwardRef()везде и считают проблему решенной. - Экспортируют приватные providers, чтобы быстро пробить границу модуля.
- Не замечают cycles из-за
index.tsbarrel imports. - Делают shared module, который знает обо всех feature modules.
Follow-up вопросы
- Когда
forwardRef()допустим? - Как
ModuleRefотличается от прямого constructor injection? - Какой module должен владеть shared use-case?
Что повторить
- Circular dependency docs:
forwardRef,ModuleRef, module forward reference. - Module boundaries and exported providers.
- Ports/adapters and domain events.
- Связанные вопросы 1, 2 и 10.
Связанные модули и карта
18. Когда использовать microservices transport в Nest?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Nest microservices transport стоит использовать, когда есть настоящая межсервисная граница, отдельный deploy/runtime и требования к messaging. Это не замена обычному module/service внутри монолита.
Что сказать на интервью (30-60 секунд)
- Я не выношу фичу в microservice "на будущее". Сначала ищу реальные причины: independent scaling, ownership, fault isolation, async workload, integration boundary.
- В Nest есть request-response и event-based message styles.
send()подходит, когда нужен ответ,emit()- когда публикуем событие и не ждем response. - Transport decision включает delivery semantics: retries, ordering, idempotency, schema versioning, observability и failure handling.
- В этом проекте queues/webhooks/microservices не описаны контрактом, поэтому нельзя утверждать конкретные transport/event semantics для LazyOffer.
Пояснение
Nest microservices abstractions поддерживают разные transporters и message patterns. Но распределенная система сложнее монолита: network failures, partial availability, duplicated messages, schema drift, retry storms, tracing across services.
Если два модуля постоянно синхронно вызывают друг друга, вы не получили независимые сервисы, а создали distributed monolith. Часто лучше оставить modular monolith с четкими boundaries и вынести только тяжелый async flow в очередь.
Сильный ответ: "Я выбираю transport по типу взаимодействия: command/request-response, event notification, stream/log, queue job. И заранее проектирую контракт и idempotency".
Мини-пример
@Controller()
export class MathController {
@MessagePattern({ cmd: 'sum' })
sum(data: number[]) {
return data.reduce((total, value) => total + value, 0);
}
}
Углубление (2-3 минуты)
Реальный кейс: генерация PDF-отчета занимает 30 секунд и может масштабироваться отдельно. Это хороший кандидат на queue/job или отдельный worker service. А вот разделение UsersModule и OrdersModule на два микросервиса только потому, что "так современно", может добавить latency, контрактные проблемы и сложные транзакции.
Для request-response важно, что caller ждет ответ и должен иметь timeout/fallback. Для event-based messaging важно, что publisher не должен зависеть от немедленной обработки, а consumer должен быть идемпотентным.
Trade-off: микросервис дает независимое масштабирование и ownership, но увеличивает операционную сложность. Если команда не готова к tracing, contract tests и incident response, сервисная граница может ухудшить надежность.
Практика
Составьте transport decision matrix:
| Сценарий | Оставить в модуле | Queue/job | Microservice request-response | Event |
|---|---|---|---|---|
| Простая CRUD-операция | да | нет | нет | нет |
| PDF export 30s | нет | да | возможно | optional |
| Проверка лимита перед оплатой | возможно | нет | да, если внешний сервис | нет |
| Уведомить аналитику о заказе | нет | возможно | нет | да |
| Reindex search | нет | да | нет | event/job |
Типичные ошибки
- Делают микросервисы без contract tests и schema versioning.
- Используют request-response там, где достаточно event/job.
- Не проектируют idempotency для event consumers.
- Получают distributed monolith с синхронными цепочками вызовов.
Follow-up вопросы
- Чем
send()отличается отemit()? - Когда очередь лучше отдельного микросервиса?
- Как обработать duplicated event?
Что повторить
@MessagePattern,@EventPattern,ClientProxy.- Request-response vs event-based messaging.
- Idempotency, ordering, retries, schema compatibility.
- Связанные вопросы 15, 16 и 20.
Связанные модули и карта
19. Как проектировать error contract для клиентских приложений?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Error contract должен стабильно объяснять клиенту тип ошибки, безопасное сообщение, связь с request и возможность retry, не раскрывая внутренние детали сервера.
Что сказать на интервью (30-60 секунд)
- Я проектирую error contract как часть API, а не как случайный output exception filter.
- Клиенту нужны stable
code, понятныйmessage, optionaldetailsдля validation,requestId, иногдаretryableилиretryAfter. - Нельзя отдавать stack trace, SQL, provider payload, token details или внутренние enum, если они не являются публичным контрактом.
- В LazyOffer сейчас нет runtime error envelope в focused contracts, поэтому контент должен говорить о принципе, а не утверждать конкретные поля проекта.
Пояснение
Nest позволяет переопределить response body через HttpException или exception filter. Но наличие filter не равно хороший contract. Хороший contract стабилен между версиями API, документирован, тестируется и понятен frontend/mobile/backend clients.
Разные ошибки требуют разной семантики: validation error помогает подсветить поле, auth error объясняет "войти" или "нет права", domain error говорит "заказ уже оплачен", dependency error может быть retryable. Если все превращается в { message: "Error" }, клиент не может построить нормальный UX.
Важно отделить public error от internal diagnostics: requestId связывает клиентский экран и серверные логи, но stack trace остается внутри.
Мини-пример
{
"code": "ORDER_ALREADY_PAID",
"message": "Order cannot be cancelled after payment.",
"requestId": "req_123",
"retryable": false
}
Углубление (2-3 минуты)
Реальный кейс: mobile app получает ошибку при оплате. Если backend возвращает только 500, клиент показывает "что-то пошло не так", пользователь жмет повторно, а платеж может уйти дважды. Если contract различает PAYMENT_PROVIDER_TIMEOUT, PAYMENT_DECLINED, ORDER_ALREADY_PAID, клиент может показать правильное действие: повторить позже, сменить карту или открыть текущий заказ.
Validation errors должны быть структурированы по полям, но domain errors - не обязательно. Auth errors должны не раскрывать лишнего: "невалидный токен" часто достаточно, а детали подписи и secret rotation остаются в логах.
Trade-off: чем богаче contract, тем лучше UX, но тем выше стоимость поддержки совместимости. Поэтому error codes должны быть стабильными, а details - аккуратно версионироваться.
Практика
Составьте error envelope compatibility table:
| Error class | Public code | HTTP meaning | Details | Retryable |
|---|---|---|---|---|
| Validation | VALIDATION_FAILED | bad input | field errors | false |
| AuthN | UNAUTHENTICATED | no valid identity | none/minimal | false |
| AuthZ | FORBIDDEN | no permission | none/minimal | false |
| Domain conflict | ORDER_ALREADY_PAID | conflict | safe domain context | false |
| Dependency timeout | PAYMENT_TIMEOUT | temporary failure | requestId only | true |
Типичные ошибки
- Меняют error code без версии и ломают client logic.
- Отдают raw exception message от базы или внешнего провайдера.
- Не различают validation/auth/domain/dependency errors.
- Не связывают client error с server logs через requestId/correlation id.
Follow-up вопросы
- Какие поля error response должны быть стабильными?
- Что нельзя отдавать клиенту даже при debug mode?
- Как версионировать error codes?
Что повторить
- Exception filters and
HttpException. - Validation/domain/auth/dependency error taxonomy.
- Request id, retryability, error code compatibility.
- Связанные вопросы 4, 6, 7 и 8.
Связанные модули и карта
20. Как мигрировать большой Express backend на Nest поэтапно?
Теги: nestjs, backend, typescript
Сложность: Middle/Senior
Короткий ответ
Большой Express backend лучше мигрировать на Nest поэтапно: сначала обернуть совместимость, выделить модульные границы, перенести один vertical slice, сохранить HTTP/error contracts и проверять parity тестами.
Что сказать на интервью (30-60 секунд)
- Я не переписываю большой backend "big bang". Сначала фиксирую текущий контракт: routes, status codes, DTO, auth, error shape, side effects.
- Затем выбираю один vertical slice: например
/orders, переношу controller/use-case/data-access, оставляя публичный API совместимым. - Для shared middleware/auth/error handling нужен compatibility layer, иначе клиенты увидят поведенческие отличия.
- Успех миграции измеряется parity tests, traffic shadowing/canary и rollback plan, а не тем, что код стал "на Nest".
Пояснение
Nest может работать поверх Express adapter, поэтому миграция часто начинается не с замены всего HTTP stack, а с постепенного переноса маршрутов и зависимостей. Опасность в том, что Express middleware, route matching, body parsing, error handling и auth semantics могут вести себя иначе.
Сильный ответ на интервью: "Я сначала делаю инвентаризацию контрактов и рисков, потом строю strangler plan: старый Express продолжает обслуживать большую часть routes, новые Nest modules забирают отдельные paths, а тесты сравнивают поведение".
Если миграция меняет error envelope, status code или auth behavior, это уже contract change и требует отдельного rollout.
Мини-пример
Phase 1: inventory routes + parity tests
Phase 2: Nest shell + shared auth/error compatibility
Phase 3: migrate /orders vertical slice
Phase 4: canary traffic + rollback
Phase 5: remove old Express route
Углубление (2-3 минуты)
Реальный кейс: Express endpoint POST /orders возвращает 409 при уже оплаченном заказе, а новый Nest filter случайно возвращает 400 с другим shape. Frontend ломается не потому, что Nest плохой, а потому что миграция нарушила контракт.
Поэтому каждый перенесенный route должен иметь parity suite: одинаковые inputs дают одинаковые status, headers where relevant, response body, auth behavior и side effects. Для рискованных routes можно включить shadow traffic: новый handler считает ответ, но не отвечает клиенту, а результат сравнивается с production behavior.
Trade-off: поэтапная миграция дольше, но снижает blast radius. Big bang быстрее на бумаге, но почти всегда опаснее для продукта.
Практика
Составьте migration strangler plan:
| Phase | Scope | Guardrail | Rollback |
|---|---|---|---|
| Inventory | routes/auth/errors | contract table | no code change |
| Shell | Nest app рядом | smoke tests | route traffic stays old |
| First slice | /orders | parity tests | switch route back |
| Canary | small traffic | error/latency compare | flag off |
| Cleanup | remove Express route | no traffic old path | restore old handler tag |
Типичные ошибки
- Начинают с переписывания папок, а не с контрактов и parity tests.
- Меняют auth/error behavior "по пути" и ломают клиентов.
- Мигрируют горизонтально: сначала все controllers, потом все services, вместо vertical slices.
- Не имеют rollback на уровне маршрута или feature flag.
Follow-up вопросы
- Как выбрать первый route для миграции?
- Что должно попасть в parity test?
- Как понять, что старый Express route можно удалить?
Что повторить
- Nest HTTP adapter, middleware, filters, pipes and guards.
- Route matching, body parsing, error contract compatibility.
- Strangler pattern, canary, shadow traffic, rollback.
- Связанные вопросы 1, 3, 6, 8 и 19.
Связанные модули и карта
Куда дальше
- Вернитесь в модуль: NestJS.
- Сверьтесь с картой темы: NestJS.
- Для q-11..q-15 пройдите bridge: Backend / Data Access Integration.
- Закрепите repository/query reasoning в SQL-песочнице: клиенты без потери нулевых заказов.
- Продолжайте по маршруту: Senior трек.