← назад к разделу

NESTJS · MODULES Фаза 7. Модули и Dependency Injection

Модули и Dependency Injection в NestJS

TypeScript · NestJS

Providers, границы модулей, scopes, динамические модули и явная композиция.

1. DI получает зависимости снаружи

1 / 5

Сервис не создаёт репозиторий, HTTP client или конфигурацию сам. Контейнер связывает интерфейсы и реализации, поэтому тест передаёт свои doubles.

  • Зависимости объявляются конструктором.
  • Контейнер владеет созданием.
  • Бизнес-код не знает composition root.
Nest containercreates providers OrderServiceconstructor(repo) Repositoryinjected

2. Модуль ограничивает состав и видимость

2 / 5

Providers, controllers, imports и exports описывают один домен. Только экспортированные провайдеры доступны другим модулям.

  • Модуль группирует связанные части.
  • Не экспортируйте всё подряд.
  • Импорт делает чужой API явным.
OrdersModule OrdersController OrdersService + repository Other modulessee exported API only

3. Scope определяет время жизни provider

3 / 5

Singleton живёт весь контекст приложения. Request scope создаёт provider на запрос, transient - на каждое внедрение.

  • Singleton обычно подходит для stateless.
  • Request scope хранит request state.
  • Transient используется для независимых экземпляров.
Singletonone per app Requestone per request Transientnew per injection Выбирайте по состоянию, не по привычке

4. Динамический модуль получает параметры

4 / 5

forRoot или register возвращает модуль с конкретными providers и exports. Конфигурация остаётся частью composition, а не скрытым синглтоном.

  • Параметры передаются явно.
  • Динамический модуль можно настроить повторно.
  • Async factory ждёт зависимости до старта.
forRoot(options)dynamic metadata DatabaseModuleDataSource providerexported Consumersconfigured API

5. Тесты подменяют границу, не контейнер

5 / 5

Unit test передаёт fake repository напрямую. Testing module проверяет wiring отдельно, поэтому доменные проверки не запускают всё приложение.

  • Используйте малый контракт.
  • Testing module проверяет DI.
  • Не требуйте БД для бизнес-правил.
OrderServicereal subject FakeRepositorysame contract Unit testno Nest app

← назад к разделу

В центре NestJS — DI-контейнер и модули: те же идеи, что в Angular, откуда NestJS их и взял. Если ты только начинаешь с NestJS, понять эти две вещи значит понять, как приложение держится вместе.

Проблема: объекты создают всё сами

Когда пишешь первый сервис, обычно делают так:

class CreateProductHandler {
  private repo = new ProductRepository(); // создаём сами
}

Проблема: CreateProductHandler намертво привязан к конкретному ProductRepository. Его не подменить в тестах, не настроить через конфигурацию. Когда сервисов становится больше десяти, паутина new внутри друг друга превращается в источник сложно отслеживаемых ошибок.

Решение — передавать зависимости снаружи, а не создавать внутри. Именно это делает DI-контейнер NestJS.

Провайдеры и DI-контейнер

Провайдер — это класс с декоратором @Injectable(). Он говорит контейнеру: «этим объектом управляй ты». Контейнер сам создаёт экземпляр и подставляет его туда, где он нужен.

import { Injectable } from '@nestjs/common';

@Injectable()
export class ProductRepository {
  async find(id: number): Promise<Product | null> { /* ... */ }
}

@Injectable()
export class CreateProductHandler {
  constructor(private readonly repo: ProductRepository) {}

  async handle(command: CreateProductCommand): Promise<Product> { /* ... */ }
}

CreateProductHandler не создаёт репозиторий сам — он его получает. Контейнер видит тип ProductRepository в конструкторе и подставляет готовый экземпляр. Зависимости объявляются просто: пишешь тип в параметре конструктора — контейнер разберётся.

Чтобы TypeScript-типы были видны в рантайме, NestJS читает метаданные через reflect-metadata. Это работает автоматически при включённом emitDecoratorMetadata: true в tsconfig.json — стандартный шаблон NestJS уже содержит эту настройку.

Модуль как граница

Провайдеры не висят в воздухе — они принадлежат модулю. Модуль (@Module) объявляет, что в него входит и что он отдаёт наружу.

import { Module } from '@nestjs/common';

@Module({
  controllers: [ProductController],
  providers: [CreateProductHandler, ProductRepository],
  exports: [ProductRepository],
})
export class ProductModule {}

Четыре поля:

ПолеЧто означает
providersчто доступно внутри модуля
controllersHTTP-контроллеры модуля
importsдругие модули, чьи экспорты нужны
exportsчто этот модуль отдаёт тем, кто его импортирует

ProductModule не видит внутренностей OrderModule, пока тот явно что-то не экспортировал через exports. Это граница: снаружи — только то, что разрешено.

Всё приложение собирается в корневом AppModule, который импортирует все остальные модули:

@Module({
  imports: [ProductModule, OrderModule, ConfigModule.forRoot()],
})
export class AppModule {}

Scopes — сколько живёт провайдер

По умолчанию провайдер — одиночка (Scope.DEFAULT): один экземпляр на всё приложение. Это правильный выбор почти всегда: провайдер без внутреннего состояния можно безопасно переиспользовать во всех запросах.

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestContext { /* данные конкретного запроса */ }

Два дополнительных scope:

  • Scope.REQUEST — новый экземпляр на каждый HTTP-запрос. Нужен, когда провайдер держит данные конкретного запроса (например, текущего пользователя). Имеет цену: «заражает» зависящие от него провайдеры тем же scope, поэтому берут его только по необходимости.
  • Scope.TRANSIENT — новый экземпляр на каждое внедрение. Нужен редко.

Динамические модули

Когда модуль нужно настраивать при подключении — передать строку подключения, ключ API или другие параметры — используют динамический модуль. Это статический метод, который возвращает описание модуля.

import { DynamicModule, Module } from '@nestjs/common';

@Module({})
export class StorageModule {
  static forRoot(options: StorageOptions): DynamicModule {
    return {
      module: StorageModule,
      providers: [
        { provide: STORAGE_OPTIONS, useValue: options },
        StorageService,
      ],
      exports: [StorageService],
    };
  }
}

// использование:
@Module({
  imports: [StorageModule.forRoot({ bucket: 'my-bucket' })],
})
export class AppModule {}

Это шаблон forRoot/forFeature, который встречается у TypeOrmModule, ConfigModule и других популярных модулей NestJS. forRoot — для глобальной настройки один раз, forFeature — для подключения в конкретном модуле с уточнёнными параметрами.

Коротко

  • Провайдер — класс с @Injectable(), которым управляет контейнер. Зависимости объявляются типами в конструкторе.
  • Модуль — граница видимости: провайдеры доступны только внутри своего модуля, наружу — только через exports.
  • Корневой AppModule собирает все остальные модули вместе.
  • Scope по умолчанию — одиночка на всё приложение; REQUEST берут только когда нужны данные конкретного запроса.
  • Динамический модуль — статический метод forRoot/forFeature для настраиваемых модулей.

Что пощупать

В стартовом каталоге практикума remodov/marketplace-system-nodejs один ProductModule собирает контроллер, сервис и репозиторий. Хранилище и кэш отдаются по токенам PRODUCT_STORE и CACHE через useFactory, поэтому тест подменяет хранилище вызовом overrideProvider, а сервис про подмену не знает.

Код: product.module.ts, cache.module.ts, cache.spec.ts.

Что почитать дальше