NestJS: провайдеры и инъекция зависимостей

В NestJS вся архитектура приложения строится вокруг системы инъекции зависимостей. Центральную роль в ней играют провайдеры — классы или значения, которые могут быть внедрены в другие компоненты через механизм DI-контейнера.

Провайдер в NestJS — это сущность, которую контейнер может создать, хранить и передавать в другие части приложения. Чаще всего провайдер реализуется в виде класса, помеченного декоратором @Injectable().

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

@Injectable()
export class UsersService {
  getUsers() {
    return ['Alice', 'Bob'];
  }
}

Здесь UsersService становится провайдером, доступным для внедрения в другие классы.


Контейнер инъекции зависимостей

В основе NestJS лежит IoC-контейнер (Inversion of Control Container). Он отвечает за:

  • создание экземпляров провайдеров
  • управление их жизненным циклом
  • разрешение зависимостей между ними
  • кэширование экземпляров (singleton по умолчанию)

Контейнер анализирует зависимости через типы параметров конструктора.

import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.getUsers();
  }
}

Здесь UsersController не создаёт UsersService вручную. Контейнер сам внедряет его в конструктор.


Регистрация провайдеров в модуле

Чтобы NestJS мог управлять провайдером, его необходимо зарегистрировать в модуле.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Массив providers сообщает контейнеру, какие классы доступны для инъекции в рамках модуля.


Принцип работы инъекции зависимостей

Инъекция зависимостей в NestJS основана на отражении типов (reflection metadata). При создании экземпляра класса контейнер:

  1. Считывает список параметров конструктора
  2. Определяет их типы
  3. Находит зарегистрированные провайдеры
  4. Создаёт или переиспользует экземпляры
  5. Передаёт их в конструктор
constructor(private readonly usersService: UsersService)

Тип UsersService является ключом для поиска соответствующего провайдера.


Скоупы провайдеров

По умолчанию все провайдеры имеют скоуп singleton — один экземпляр на всё приложение.

Существуют три основных типа скоупов:

Singleton

Создаётся один раз и переиспользуется.

@Injectable()
export class UsersService {}

Request scope

Создаётся новый экземпляр на каждый HTTP-запрос.

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

@Injectable({ scope: Scope.REQUEST })
export class UsersService {}

Такой подход увеличивает нагрузку, но позволяет хранить данные запроса внутри сервиса.

Transient scope

Создаётся новый экземпляр каждый раз при внедрении.

@Injectable({ scope: Scope.TRANSIENT })
export class UsersService {}

Пользовательские провайдеры

NestJS позволяет регистрировать провайдеры не только через классы, но и через фабрики, значения и алиасы.

Провайдер через значение

const config = {
  host: 'localhost',
};

@Module({
  providers: [
    {
      provide: 'CONFIG',
      useValue: config,
    },
  ],
})
export class AppModule {}

Инъекция:

constructor(@Inject('CONFIG') private config) {}

Фабричные провайдеры

Фабрика позволяет динамически создавать зависимости.

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useFactory: () => {
        return createConnection();
      },
    },
  ],
})
export class DatabaseModule {}

Фабрика может зависеть от других провайдеров:

{
  provide: 'CONNECTION',
  useFactory: (configService: ConfigService) => {
    return createConnection(configService.get('db'));
  },
  inject: [ConfigService],
}

Асинхронные провайдеры

Некоторые зависимости требуют асинхронной инициализации.

{
  provide: 'ASYNC_DATA',
  useFactory: async () => {
    const data = await fetchData();
    return data;
  },
}

NestJS дождётся завершения Promise перед регистрацией провайдера.


Алиасы провайдеров

Можно создать несколько токенов для одной и той же зависимости.

{
  provide: 'PRIMARY_SERVICE',
  useClass: UsersService,
},
{
  provide: 'USERS_SERVICE',
  useExisting: 'PRIMARY_SERVICE',
}

Оба токена будут указывать на один экземпляр.


Инъекция через @Inject

Когда тип не может быть использован как ключ (строки, интерфейсы), применяется декоратор @Inject.

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

@Injectable()
export class AppService {
  constructor(@Inject('CONFIG') private config: any) {}
}

Кастомные токены и интерфейсы

TypeScript интерфейсы исчезают в runtime, поэтому для них используются токены.

export const USER_REPOSITORY = 'USER_REPOSITORY';
{
  provide: USER_REPOSITORY,
  useClass: MongoUserRepository,
}
constructor(@Inject(USER_REPOSITORY) private repo: UserRepository) {}

Циклические зависимости

Циклические зависимости возникают, когда два провайдера зависят друг от друга.

@Injectable()
export class AService {
  constructor(private b: BService) {}
}

@Injectable()
export class BService {
  constructor(private a: AService) {}
}

Решение — forwardRef.

@Injectable()
export class AService {
  constructor(
    @Inject(forwardRef(() => BService))
    private b: BService,
  ) {}
}

И аналогично в обратную сторону.


Лайфсайклы провайдеров

Провайдеры могут реагировать на события жизненного цикла приложения.

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

@Injectable()
export class UsersService implements OnModuleInit {
  onModuleInit() {
    // инициализация логики
  }
}

Также доступны:

  • OnApplicationBootstrap
  • OnModuleDestroy
  • BeforeApplicationShutdown

Область видимости модулей и провайдеров

Провайдер существует в рамках модуля, в котором он объявлен. Чтобы использовать его в другом модуле, его необходимо экспортировать.

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

И импортировать в другом модуле:

@Module({
  imports: [UsersModule],
})
export class AppModule {}

Глобальные провайдеры

Провайдер можно сделать глобальным:

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

@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

После этого его не нужно импортировать в каждом модуле.


Паттерны использования провайдеров

Сервисный слой

Провайдеры чаще всего используются как сервисы бизнес-логики.

Репозитории

Абстракция доступа к данным через интерфейс.

Инфраструктурные сервисы

  • логирование
  • работа с очередями
  • внешние API

Конфигурационные провайдеры

Централизованное управление настройками приложения


Расширенные механизмы контейнера

NestJS DI-контейнер поддерживает:

  • рефлексию типов
  • динамическую регистрацию провайдеров
  • переопределение зависимостей (override providers)
  • модульную изоляцию контекста

Это позволяет строить масштабируемые архитектуры с чётким разделением ответственности и слабой связностью компонентов.