В NestJS вся архитектура приложения строится вокруг системы инъекции зависимостей. Центральную роль в ней играют провайдеры — классы или значения, которые могут быть внедрены в другие компоненты через механизм DI-контейнера.
Провайдер в NestJS — это сущность, которую контейнер может создать,
хранить и передавать в другие части приложения. Чаще всего провайдер
реализуется в виде класса, помеченного декоратором
@Injectable().
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
getUsers() {
return ['Alice', 'Bob'];
}
}
Здесь UsersService становится провайдером, доступным для
внедрения в другие классы.
В основе NestJS лежит IoC-контейнер (Inversion of Control Container). Он отвечает за:
Контейнер анализирует зависимости через типы параметров конструктора.
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). При создании экземпляра класса контейнер:
constructor(private readonly usersService: UsersService)
Тип UsersService является ключом для поиска
соответствующего провайдера.
По умолчанию все провайдеры имеют скоуп singleton — один экземпляр на всё приложение.
Существуют три основных типа скоупов:
Создаётся один раз и переиспользуется.
@Injectable()
export class UsersService {}
Создаётся новый экземпляр на каждый HTTP-запрос.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class UsersService {}
Такой подход увеличивает нагрузку, но позволяет хранить данные запроса внутри сервиса.
Создаётся новый экземпляр каждый раз при внедрении.
@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.
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() {
// инициализация логики
}
}
Также доступны:
OnApplicationBootstrapOnModuleDestroyBeforeApplicationShutdownПровайдер существует в рамках модуля, в котором он объявлен. Чтобы использовать его в другом модуле, его необходимо экспортировать.
@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 {}
После этого его не нужно импортировать в каждом модуле.
Провайдеры чаще всего используются как сервисы бизнес-логики.
Абстракция доступа к данным через интерфейс.
Централизованное управление настройками приложения
NestJS DI-контейнер поддерживает:
Это позволяет строить масштабируемые архитектуры с чётким разделением ответственности и слабой связностью компонентов.