open atlas
← Все проекты

backend · advanced · 9d

Nest-сервис в продакшен-форме

NestJS вознаграждает за структуру и наказывает за её отсутствие. Собери сервис так, как его выкатила бы команда: feature-модули с настоящими границами, DTO, валидирующие на краю, guards и interceptors, которые держат сквозные заботы, типизированный config, в котором не опечатаешься, и e2e-набор, поднимающий всё приложение целиком. Это и есть разница между «знаю декораторы Nest» и «понимаю, зачем они существуют».

Накидать каркас Nest-приложения может каждый; сеньорский навык — понимать, почему каждый декоратор заслуживает своего места. Этот проект заставляет защищать каждый из них: модули — потому что границы держат код изменяемым, DTO — потому что край единственное честное место для валидации, guards и interceptors — потому что сквозная логика гниёт, когда её копипастят, типизированный config — потому что окружения врут, а e2e-тесты — потому что связность ломается так, как юниты никогда не увидят. Очередь и связанные логи — это момент, когда оно перестаёт быть учебным приложением и начинает вести себя как что-то «на дежурстве»: работа уходит за пределы запроса, повторная доставка не должна списать дважды, а один id позволяет проследить единственный запрос через два процесса. Собери так один раз — и больше никогда не будешь гадать, от чего тебя оберегает структура Nest.

Результат

Работающий NestJS-сервис: минимум два feature-модуля, входные DTO, валидируемые через class-validator, глобально подключённые guard и interceptor, типизированный и проверяемый по схеме config, а также e2e-набор на Jest, который поднимает приложение по HTTP и проверяет как успешные, так и ошибочные сценарии.

Этапы

0/6 · 0%
  1. 01Разметь модули

    Не поддавайся искушению свалить всё в AppModule. Возьми две настоящие фичи — скажем, «orders» и «notifications» — и дай каждой свой модуль со своим контроллером и провайдером, экспортируя только то, что другому модулю разрешено трогать. Дисциплина в том, что публичная поверхность модуля — это его exports; всё остальное приватно. Используй DI Nest, чтобы внедрять сервис в контроллер, а не конструировать его руками, — тогда временем жизни управляет контейнер, и в тестах можно подменять реализации. К концу ты должен уметь ткнуть в любой класс и сказать, какой модуль им владеет и почему.

    Критерии готовности
    • Существуют два feature-модуля, каждый экспортирует только свой публичный провайдер; импорт приватного провайдера модуля извне не проходит.
    • Сервис внедряется в свой контроллер через DI-контейнер, а не создаётся через new.
  2. 02Валидируй на краю

    Каждый байт от клиента враждебен, пока не доказано обратное. Опиши DTO-классы с декораторами class-validator и включи глобальный ValidationPipe с whitelist и forbidNonWhitelisted, чтобы неизвестные поля отсекались или отклонялись, а не тихо протекали в обработчики. Выгода в том, что появляется единое декларативное место, где форма запроса и есть контракт: плохое тело отлетает с 400, называя поле-нарушитель ещё до того, как запустится бизнес-логика. Осознанно реши, включать ли transform и приведение типов, потому что «whitelist on, transform off» и «оба on» ведут себя по-разному и это всплывёт позже.

    Критерии готовности
    • Запрос с неизвестным или неверно типизированным полем возвращает 400 с сообщением, называющим поле, и никогда не доходит до тела контроллера.
    • Валидные запросы проходят с полностью типизированным экземпляром DTO, а не сырым нетипизированным объектом.
  3. 03Возьми сквозные заботы под контроль

    Авторизация и логирование не должны быть размазаны по контроллерам. Напиши guard, который решает, можно ли пропустить запрос — вернёт false или бросит исключение, чтобы отдать 403 ещё до запуска обработчика, — и interceptor, который оборачивает обработчик, чтобы замерить время или преобразовать ответ. Важно понимать порядок: guards выполняются раньше pipes и interceptors, а interceptors обхватывают обработчик с обеих сторон. Подключи хотя бы один глобально, а другой ограничь одним маршрутом — почувствуй разницу между общей политикой приложения и локальным поведением. Смысл в том, что Nest даёт эти швы именно для того, чтобы бизнес-код оставался про бизнес.

    Критерии готовности
    • Guard блокирует неавторизованный запрос с 403 до запуска обработчика; авторизованный проходит дальше.
    • Interceptor заметно оборачивает обработчик (например, добавляет заголовок с длительностью или единый конверт ответа) на каждом ответе, который покрывает.
  4. 04Конфигурация, в которой не опечатаешься

    Читать process.env напрямую — мина замедленного действия: опечатка в ключе даёт undefined в три часа ночи, а не на старте. Загружай конфигурацию через ConfigModule и валидируй её по схеме (zod или joi) при загрузке, чтобы отсутствующее или некорректное значение роняло приложение сразу с понятным сообщением, а не падало глубоко внутри обработчика позже. Открой типизированный config-сервис, чтобы вызывающий код спрашивал config.get('database.url') и получал string, а не string | undefined. Цель — fail-fast: процесс должен отказываться стартовать, пока его окружение не доказуемо полно и корректно типизировано.

    Критерии готовности
    • Запуск приложения с отсутствующей или некорректной обязательной env-переменной падает сразу с сообщением, называющим переменную.
    • Конфигурация читается через типизированный сервис, у которого типы возврата для обязательных ключей ненулевые.
  5. 05Подними всё целиком в тестах

    Юнит-тесты доказывают логику провайдера; e2e-тесты доказывают связность. Возьми тестовые утилиты Nest, чтобы собрать реальный граф модулей, поднять HTTP-сервер и дёргать эндпоинты через supertest — pipes, guards и interceptors в работе, ровно как в проде. Намеренно покрой несчастливые пути: проверь, что невалидный DTO даёт 400, неавторизованный запрос — 403, а валидный возвращает правильное тело. Этот набор — контракт, против которого можно рефакторить без страха: пока он зелёный, построенные швы держат.

    Критерии готовности
    • E2e-тест поднимает приложение по HTTP и проверяет успешный 2xx с ожидаемым телом.
    • Отдельные e2e-тесты проверяют ошибочные пути 400 (невалидный DTO) и 403 (guard).
  6. 06Сеньорский рывок: очередь и лог, которому можно верить

    Настоящие системы не делают всё прямо в запросе. Вынеси вторую фичу за message-транспорт: HTTP-модуль принимает работу и публикует сообщение, а consumer-микросервис (транспорт Redis или RabbitMQ через @nestjs/microservices) обрабатывает её асинхронно. Теперь ты честно сталкиваешься с семантикой доставки — это fire-and-forget, at-least-once, не спишет ли повторная доставка деньги дважды? — и намеренно делаешь consumer идемпотентным. Дополни это структурированным JSON-логированием (pino), которое проносит correlation id от входящего запроса до consumer, чтобы один запрос был одной прослеживаемой историей через два процесса. Именно эта связность превращает «где-то медленно» в «медленно вот здесь».

    Критерии готовности
    • Работа течёт от HTTP-эндпоинта через очередь к consumer-микросервису, который обрабатывает её асинхронно.
    • Логи — структурированный JSON, и единый correlation id связывает входящий запрос с его обработкой на стороне consumer.

Рубрика

Джуниор Миддл Сеньор
Границы модулей и дисциплина DI Большая часть логики живёт в AppModule; провайдеры конструируются через new или свободно импортируются между модулями, реальной инкапсуляции нет. Два или более feature-модулей каждый экспортирует только свой публичный провайдер; сервис внедряется через DI-контейнер, а не создаётся руками; импорт приватного провайдера извне модуля падает при сборке. Scope провайдера выбирается намеренно (DEFAULT / REQUEST / TRANSIENT) с письменным обоснованием каждого не-дефолтного выбора; границы модулей делают граф зависимостей ациклическим, а публичная поверхность каждого модуля — это минимальный API, нужный его потребителям, а не удобный реэкспорт всего, что внутри.
Валидация DTO на краю через class-validator Валидация ручная (if-проверки внутри обработчика) или отсутствует; неизвестные поля тихо протекают в бизнес-логику. Глобальный ValidationPipe с whitelist: true и forbidNonWhitelisted: true отвергает неизвестные поля на границе; невалидные запросы возвращают 400 с сообщением, называющим поле-нарушитель, ещё до запуска обработчика. transform: true против transform: false — явное решение, а не случайный дефолт; ты можешь описать, что делает «whitelist on, transform off» с полем типа number, присланным как строка, а классы DTO служат источниками TypeScript-типов — параллельного интерфейса для синхронизации нет.
Guards, interceptors и типизированная конфигурация Логика аутентификации и замер времени размазаны по телам контроллеров; config читается напрямую через process.env.KEY, и опечатка всплывает в рантайме, а не при старте. Guard возвращает false / бросает исключение до запуска обработчика для неавторизованных запросов; interceptor оборачивает вызов обработчика; ConfigModule валидирует схему при загрузке и падает с именованной ошибкой при отсутствующей переменной. Guard подключён глобально через один экземпляр уровня приложения, а не продублирован на каждом контроллере; rxjs-конвейер tap / catchError interceptor'а обрабатывает ошибки без их проглатывания; типизированный ConfigService возвращает не-nullable типы для обязательных ключей, так что вызывающий код никогда не получает string | undefined.
Покрытие e2e-связности и внеполосная доставка Существуют только юнит-тесты; связность модулей и порядок ValidationPipe / guard никогда не тестируются от начала до конца. E2e-набор поднимает реальный граф модулей по HTTP (supertest) и проверяет успешный 2xx, ошибочный DTO 400 и неавторизованный 403 — pipes, guards и interceptors всё в работе. Вторая фича подключена за message-транспортом; correlation id проходит от HTTP-запроса через очередь до лога consumer'а; consumer идемпотентен при повторной доставке, что доказывается тестом, отправляющим одно и то же сообщение дважды и проверяющим единственный побочный эффект.
Эталонный разбор (спойлер)

Почему границы модулей важны: модульная система Nest делает граф зависимостей явным и верифицируемым при старте. Без неё циклическая зависимость или случайно публичный провайдер всплывают как рантайм-ошибка глубоко в пользовательском запросе, а не как ошибка связности при загрузке. Пометить провайдер как не-экспортируемый — это самый дешёвый способ закрепить «это внутренняя деталь реализации».

Компромисс whitelist у ValidationPipe: whitelist: true тихо убирает неизвестные поля; forbidNonWhitelisted: true отклоняет их с 400. Правильный выбор зависит от контракта: SDK-клиент под твоим контролем оправдывает жёсткий отказ, чтобы сломанное изменение API сразу всплыло; браузерная форма может оправдывать обрезку. Случайный выбор — не то же самое, что осознанный.

Порядок выполнения под нагрузкой: guards выполняются до pipes, которые выполняются до обработчика, который оборачивается с обеих сторон interceptor'ами. Медленный tap interceptor'а добавляет задержку к каждому маршруту, который он покрывает. Guard, бросающий исключение безусловно, делает обработчик недостижимым в тестах — именно поэтому e2e-тесты, поднимающие реальный граф, ловят то, что юнит-тесты никогда не увидят.

Семантика внеполосной доставки не бесплатна: перенос работы за message-транспорт означает, что at-least-once доставка — дефолт для большинства транспортов: повторная доставка может перевыполнить обработчик. Сделать consumer идемпотентным (ключ дедупа в транзакционной записи) — не оптимизация, это требование корректности в момент, когда повторная доставка возможна.

Сделай по-сеньорски

  • Сделай consumer устойчивым к повторной доставке: дай каждому сообщению ключ, дедуплицируй по нему и докажи тестом, что обработка одного и того же сообщения дважды даёт тот же эффект, что и один раз.
  • Добавь глобальный exception filter, превращающий брошенные доменные ошибки в единый конверт ошибки, и проверь его форму в e2e-тестах, чтобы ответы об ошибках были частью контракта, а не случайностью.
  • Подключи эндпоинт /health и interceptor длительности запроса, затем нагрузь путь через очередь, чтобы увидеть, куда смещается задержка, когда работа уходит с потока запроса.

Навыки

designing feature modules with explicit boundariesDTO validation with class-validator and a global ValidationPipewriting guards and interceptors for cross-cutting concernstyped, schema-validated configuratione2e testing a Nest app over HTTPmessage-based communication over a queuestructured logging with request correlation

Рекомендуемый стек

nestjstypescriptclass-validator / class-transformer@nestjs/config + zod (or joi)jest + supertest@nestjs/microservices (Redis or RabbitMQ transport)pino (nestjs-pino)