open atlas
↑ К треку
NestJS с нуля до senior NEST · 00 · 01

Зачем NestJS и ментальная модель DI

NestJS добавляет структуру к голому Node-бэкенду: модули, контроллеры, провайдеры — и dependency injection. Ты объявляешь, что нужно классу, в конструкторе; IoC container строит и внедряет это, поэтому проводка централизована, а код остаётся тестируемым.

NEST Junior ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Через три месяца жизни голого Express-приложения в папке routes/ лежит 40 файлов, и каждый обработчик где-то наверху тянется к глобальному db и к new MailService(). Чтобы протестировать один контроллер, приходится поднимать настоящую базу — потому что контроллер жёстко зашил то, как он получает свои зависимости. Добавить кэширование значит править двенадцать файлов, в каждом из которых через new создавался один и тот же сервис. Формально всё не так уж плохо — просто нет структуры и нет шва, чтобы что-то подменить. NestJS существует, чтобы дать такому приложению хребет.

Что такое Nest и какую проблему он решает

Express и Fastify намеренно минималистичны: они маршрутизируют HTTP-запросы к функциям — и на этом всё. Для сервиса на 200 строк это идеально, для сервиса на 20 000 строк — мучительно. Без соглашения каждая команда изобретает своё — где живёт бизнес-логика, как обработчик получает клиент к базе, как сгруппированы фичи — и ответы расходятся от файла к файлу. Получается тот самый клубок из хука: логика приклеена к глобальным объектам, зависимости создаются через new где придётся, и нет чистого места, чтобы подсунуть фейк в тесте.

NestJS — это opinionated-фреймворк поверх Node (по умолчанию он использует Express, но может работать и на Fastify). Он не заменяет обработку HTTP; он её организует. Он даёт три основных строительных блока и один механизм, связывающий их:

  • Модули (modules) — контейнеры в рамках фичи (@Module({...})), которые группируют связанный код и объявляют, что они отдают наружу и что им нужно.
  • Контроллеры (controllers) — классы, чьи методы обрабатывают входящие запросы и формируют ответ. В них маршрутизация, а не бизнес-логика.
  • Провайдеры (providers) — всё, что можно внедрить: сервисы, репозитории, фабрики, клиенты. Здесь живёт бизнес-логика, помеченная @Injectable().

Вместе эти три блока дают каждой фиче постоянный адрес: модуль ею владеет, контроллер открывает её по HTTP, а провайдер делает работу. Когда ты добавляешь новую фичу — ты знаешь, куда кладёт каждый кусок, и следующий инженер, открывший проект, тоже это знает.

Механизм — это dependency injection (DI), и именно эту часть стоит усвоить в первый же день: всё остальное в Nest построено на ней.

Ментальная модель DI: объявляй, что нужно, не создавай это сам

Вот вся идея в одном предложении: класс перечисляет то, что ему нужно, в конструкторе, а предоставлять это — задача чего-то другого. Ты перестаёшь писать new для своих зависимостей.

Без DI контроллер сам тянется наружу и создаёт своих соавторов:

// Ручная проводка — контроллер сам решает, КАК он получает зависимости
class UsersController {
  private service: UsersService;
  constructor() {
    const db = new Database(process.env.DB_URL);
    this.service = new UsersService(db); // жёстко зашито, нельзя подменить
  }
}

Теперь контроллер приварен к конкретному UsersService и к настоящему Database. Чтобы протестировать его, придётся дать настоящую базу; чтобы изменить то, как строится UsersService, придётся править каждое место, где его создавали.

С DI в Nest контроллер просто объявляет зависимость как параметр конструктора. Он никогда не говорит, как сервис создаётся:

import { Injectable, Controller, Get, Module } from "@nestjs/common";

@Injectable()
class UsersService {
  findAll() {
    return [{ id: 1, name: "Ada" }];
  }
}

@Controller("users")
class UsersController {
  // Объявлено, а не создано. Экземпляр поставляет Nest.
  constructor(private readonly users: UsersService) {}

  @Get()
  list() {
    return this.users.findAll();
  }
}

@Module({
  controllers: [UsersController],
  providers: [UsersService], // регистрируем то, что можно внедрять
})
export class UsersModule {}

Когда приложение стартует, IoC container (Inversion of Control container, контейнер инверсии управления) читает сигнатуры конструкторов, вычисляет граф зависимостей, создаёт каждый provider один раз и передаёт экземпляры внутрь. «Инверсия управления» — это и есть название для такого переворота: созданием управляет фреймворк, а не твой код. Ты объявил UsersService в providers модуля, поэтому, когда контейнер строит UsersController, он видит, что конструктору нужен UsersService, создаёт (или переиспользует) его и внедряет.

Почему это работает

Откуда Nest знает, какой класс внедрить из private readonly users: UsersService? В TypeScript при включённом emitDecoratorMetadata компилятор записывает типы параметров конструктора как метаданные, которые Nest читает в рантайме через reflect-metadata. Поэтому аннотация типа здесь — не просто документация, а injection token. Для зависимостей, которые не являются классом (значение конфига, интерфейс, сторонний клиент), используют custom providers с явным токеном и @Inject() — это подробно разобрано в документации по fundamentals.

Почему это важно: тестирование, проводка, связанность, lifecycle

Выгода от «объявляй, не создавай» проявляется в четырёх конкретных вещах.

Тестируемость. Поскольку контроллер знает лишь, что ему нужно нечто формы UsersService, тест может подсунуть мок. Тестовый модуль Nest позволяет переопределить provider, чтобы прогнать потребителя с фейком — без настоящей базы, без сети. Тот шов, которого не хватало в хуке, теперь встроен.

const moduleRef = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [UsersService],
})
  .overrideProvider(UsersService)
  .useValue({ findAll: () => [{ id: 1, name: "Mock" }] }) // подставляем фейк
  .compile();

Единый источник проводки. Создание описано один раз, в метаданных модуля, а не разбросано по каждому потребителю. Добавить зависимость в UsersService — это правка одной строки в его конструкторе; никто из тех, кто использует сервис, не меняется.

Слабая связанность (loose coupling). Потребители зависят от того, что provider делает, а не от того, как он собран. Это делает сервисы композируемыми и заменяемыми — кэширование, логирование, другая реализация за тем же токеном — без правки мест вызова.

Управление lifecycle. Контейнер владеет временем жизни каждого provider’а. По умолчанию provider — синглтон: создаётся один раз и разделяется, что и нужно для stateless-сервиса или пула соединений. Nest также вызывает lifecycle-хуки (onModuleInit, onApplicationShutdown), чтобы ресурсы запускались и останавливались в порядке зависимостей.

Когда брать Nest — и когда это перебор

Структура Nest — это рычаг на большой, долгоживущей кодовой базе и чистый оверхед на крошечной. Решение — про размер, срок жизни и команду, а не про моду.

АспектГолый Express / FastifyNestJS
СтруктураИзобретаешь сам; расходится по командамМодули/контроллеры/провайдеры, навязаны
Dependency injectionРучной new или самописный контейнерВстроенный IoC container
Шов для тестовСамописные моки, настоящие зависимости лезут внутрьoverrideProvider подменяет фейки
BoilerplateМинимум на стартеБольше заранее (декораторы, модули)
Кривая обученияНизкаяВыше (DI, декораторы, соглашения)

Структурированный TypeScript-бэкенд, команда, которой нужны общие соглашения, или сервис, который ты будешь поддерживать годами — всё это многократно окупает первоначальную церемонию. Однофайловый скрипт, одна serverless-функция или одноразовый прототип — нет: декораторы и проводка модулей там чистый налог, а голый Express-обработчик или обычная функция выкатятся быстрее. Перед выбором спроси себя: будет ли эта кодовая база понятна другому инженеру через два года? Если да — соглашения Nest отобьют затраты с лихвой.

Выбери лучший вариант

Команда начинает долгоживущий внутренний API на TypeScript: много эндпоинтов, несколько общих сервисов (auth, db, mailer), 5 инженеров, проект на годы. Выбери основу.

Викторина

В Nest у контроллера есть constructor(private readonly users: UsersService), и UsersService указан в providers модуля. Откуда берётся экземпляр UsersService?

Викторина

В чём главная выгода для тестируемости от объявления зависимости в конструкторе вместо new внутри класса?

Вспомните перед уходом
  1. 01
    Объясни ментальную модель DI в Nest и почему она лучше, чем new зависимостей внутри класса.
  2. 02
    Когда NestJS — верный выбор, а когда перебор?
Итог

Голые Express и Fastify маршрутизируют запросы — и на этом всё, что оставляет растущий бэкенд без общей структуры и без шва для тестирования: бизнес-логика приклеена к глобалам, а зависимости создаются через new где придётся. NestJS встаёт поверх Node (по умолчанию Express, опционально Fastify) и добавляет эту структуру через модули, контроллеры и провайдеры, связанные dependency injection. Ментальная модель DI — её сердце: класс объявляет, что ему нужно, в конструкторе и никогда это не создаёт; IoC container читает типы конструктора, разрешает граф зависимостей, создаёт каждый provider один раз и внедряет экземпляры — инверсия управления означает, что созданием владеет фреймворк, а не твой код. Этот один сдвиг покупает тестируемость (переопредели provider моком, без настоящей базы), единое центральное место для проводки, слабую связанность между потребителями и реализациями и управляемый контейнером lifecycle. Бери Nest на больших, долгоживущих, командных TypeScript-бэкендах, где такая структура окупает свою первоначальную церемонию, и пропускай для крошечных скриптов и одноразовых функций, где обычный обработчик быстрее. Теперь, когда ты встретишь конструктор вида constructor(private readonly users: UsersService), ты сразу увидишь суть: класс объявляет потребность, IoC container её удовлетворяет — никакого new, никаких глобалов, никакой проводки, разбросанной по всей кодовой базе.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.