Пошук уроків, статей та іншого контенту
Налаштуєте useValue, useClass, useFactory і useExisting для керування залежностями поза стандартним механізмом NestJS.
NestJS зазвичай створює залежність на основі класу:
@Injectable()
export class UserService {
constructor(private readonly repository: UserRepository) {}
}У такому випадку NestJS:
знаходить UserRepository у списку провайдерів;
створює його екземпляр;
передає екземпляр у конструктор UserService.
Однак залежність не завжди є класом, який можна просто створити через new. Наприклад, це можуть бути:
об’єкт конфігурації;
значення з environment-змінних;
клієнт зовнішнього сервісу;
одна з кількох реалізацій інтерфейсу;
вже зареєстрований провайдер під іншим токеном;
результат фабричної функції.
Для таких випадків NestJS підтримує власні провайдери.
Провайдер ідентифікується токеном. Токеном може бути:
клас;
рядок;
Symbol;
інше значення, яке підтримує DI-контейнер.
Наприклад:
const APP_CONFIG = Symbol('APP_CONFIG');Той самий токен потрібно використовувати під час реєстрації провайдера та його ін’єкції:
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
environment: 'development',
},
},
],
})
export class AppModule {}@Injectable()
export class AppService {
constructor(
@Inject(APP_CONFIG)
private readonly config: { environment: string },
) {}
}Для токенів, які не є класами, потрібен декоратор @Inject().
useValueuseValue реєструє готове значення як провайдер. NestJS не створює цей об’єкт, а використовує його безпосередньо.
Це зручно для:
конфігурації;
констант;
mock-об’єктів у тестах;
готових екземплярів класів;
простих об’єктів із налаштуваннями.
const APP_CONFIG = Symbol('APP_CONFIG');
const appConfig = {
environment: 'development',
port: 3000,
};
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: appConfig,
},
],
})
export class AppModule {}Залежність отримується через токен:
@Injectable()
export class AppService {
constructor(
@Inject(APP_CONFIG)
private readonly config: {
environment: string;
port: number;
},
) {}
printConfig(): void {
console.log(this.config.environment);
console.log(this.config.port);
}
}useValueNestJS не змінює і не клонує значення, передане через useValue. Якщо це об’єкт, усі споживачі отримають посилання на той самий об’єкт.
Тому конфігурацію часто роблять незмінною:
const appConfig = Object.freeze({
environment: 'development',
port: 3000,
});useClassuseClass вказує NestJS, який клас потрібно створити для певного токена.
Це корисно, коли код залежить не від конкретного класу, а від абстрактного токена. Реалізацію можна змінити в одному місці.
const NOTIFIER = Symbol('NOTIFIER');
@Injectable()
export class ConsoleNotifier {
send(message: string): void {
console.log(`[Console] ${message}`);
}
}
@Module({
providers: [
{
provide: NOTIFIER,
useClass: ConsoleNotifier,
},
],
})
export class AppModule {}Тепер клас-споживач залежить від токена, а не від ConsoleNotifier напряму:
@Injectable()
export class NotificationService {
constructor(
@Inject(NOTIFIER)
private readonly notifier: ConsoleNotifier,
) {}
notify(): void {
this.notifier.send('Операцію виконано');
}
}У майбутньому реалізацію можна замінити:
@Injectable()
export class EmailNotifier {
send(message: string): void {
console.log(`[Email] ${message}`);
}
}
@Module({
providers: [
{
provide: NOTIFIER,
useClass: EmailNotifier,
},
],
})
export class AppModule {}Код NotificationService при цьому не змінюється.
Ці два записи можуть бути еквівалентними:
@Module({
providers: [
ConsoleNotifier,
],
})
export class AppModule {}@Module({
providers: [
{
provide: ConsoleNotifier,
useClass: ConsoleNotifier,
},
],
})
export class AppModule {}Але useClass стає особливо корисним, коли токен відділений від конкретної реалізації:
{
provide: NOTIFIER,
useClass: ConsoleNotifier,
}useFactoryuseFactory створює значення за допомогою функції. Фабрика може:
приймати залежності;
виконувати умовну логіку;
повертати об’єкт;
бути асинхронною;
створювати клієнт зовнішнього сервісу.
Залежності фабрики вказуються в масиві inject.
const APP_CONFIG = Symbol('APP_CONFIG');
const API_CLIENT = Symbol('API_CLIENT');
type AppConfig = {
apiUrl: string;
apiKey: string;
};
type ApiClient = {
request(path: string): string;
};
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
apiKey: 'development-key',
} satisfies AppConfig,
},
{
provide: API_CLIENT,
inject: [APP_CONFIG],
useFactory: (config: AppConfig): ApiClient => {
return {
request(path: string): string {
return `Запит до ${config.apiUrl}${path} з ключем ${config.apiKey}`;
},
};
},
},
],
})
export class AppModule {}У цьому прикладі NestJS спочатку знаходить APP_CONFIG, а потім передає його у фабрику для створення API_CLIENT.
Ін’єкція фабричного результату виглядає так:
@Injectable()
export class DataService {
constructor(
@Inject(API_CLIENT)
private readonly apiClient: ApiClient,
) {}
load(): void {
console.log(this.apiClient.request('/users'));
}
}Фабрика може повертати Promise. NestJS дочекається результату під час ініціалізації модуля.
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
// Тут може бути асинхронне підключення до бази даних
return createDatabaseConnection();
},
}Асинхронна фабрика особливо корисна для залежностей, які потрібно підготувати до запуску застосунку.
Усі залежності передаються в тому самому порядку, у якому вони вказані в inject:
{
provide: 'SERVICE_OPTIONS',
inject: ['APP_CONFIG', 'LOGGER'],
useFactory: (
config: AppConfig,
logger: Logger,
) => {
logger.log(`Запуск у режимі ${config.environment}`);
return {
timeout: config.timeout,
};
},
}Порядок параметрів фабрики має відповідати порядку токенів у inject.
useExistinguseExisting створює псевдонім для вже зареєстрованого провайдера. Новий екземпляр при цьому не створюється.
const NOTIFIER = Symbol('NOTIFIER');
const AUDIT_NOTIFIER = Symbol('AUDIT_NOTIFIER');
@Module({
providers: [
{
provide: NOTIFIER,
useClass: ConsoleNotifier,
},
{
provide: AUDIT_NOTIFIER,
useExisting: NOTIFIER,
},
],
})
export class AppModule {}Обидва токени посилаються на той самий екземпляр:
@Injectable()
export class AuditService {
constructor(
@Inject(AUDIT_NOTIFIER)
private readonly notifier: ConsoleNotifier,
) {}
}Це відрізняється від useClass:
{
provide: NOTIFIER,
useClass: ConsoleNotifier,
},
{
provide: AUDIT_NOTIFIER,
useClass: ConsoleNotifier,
}У такому варіанті NestJS створить два окремі екземпляри ConsoleNotifier.
За допомогою useExisting буде один екземпляр:
{
provide: NOTIFIER,
useClass: ConsoleNotifier,
},
{
provide: AUDIT_NOTIFIER,
useExisting: NOTIFIER,
}Нижче наведено застосунок, який використовує всі чотири способи створення власних провайдерів:
useValue — конфігурація;
useClass — реалізація сповіщувача;
useFactory — створення форматованого повідомлення;
useExisting — псевдонім для сповіщувача.
import { Inject, Injectable, Module } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
const APP_CONFIG = Symbol('APP_CONFIG');
const NOTIFIER = Symbol('NOTIFIER');
const AUDIT_NOTIFIER = Symbol('AUDIT_NOTIFIER');
const MESSAGE_BUILDER = Symbol('MESSAGE_BUILDER');
type AppConfig = {
environment: string;
applicationName: string;
};
type Notifier = {
send(message: string): void;
};
type MessageBuilder = {
build(action: string): string;
};
@Injectable()
class ConsoleNotifier implements Notifier {
send(message: string): void {
console.log(`[Notifier] ${message}`);
}
}
@Injectable()
class AppService {
constructor(
@Inject(NOTIFIER)
private readonly notifier: Notifier,
@Inject(AUDIT_NOTIFIER)
private readonly auditNotifier: Notifier,
@Inject(MESSAGE_BUILDER)
private readonly messageBuilder: MessageBuilder,
) {}
run(): void {
const message = this.messageBuilder.build('Створення користувача');
this.notifier.send(message);
this.auditNotifier.send(`Аудит: ${message}`);
console.log(
'Обидва токени посилаються на той самий екземпляр:',
this.notifier === this.auditNotifier,
);
}
}
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
environment: 'development',
applicationName: 'users-api',
} satisfies AppConfig,
},
{
provide: NOTIFIER,
useClass: ConsoleNotifier,
},
{
provide: AUDIT_NOTIFIER,
useExisting: NOTIFIER,
},
{
provide: MESSAGE_BUILDER,
inject: [APP_CONFIG],
useFactory: (config: AppConfig): MessageBuilder => {
return {
build(action: string): string {
return `[${config.applicationName}:${config.environment}] ${action}`;
},
};
},
},
AppService,
],
})
class AppModule {}
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
app.get(AppService).run();
await app.close();
}
void bootstrap();Результат роботи матиме приблизно такий вигляд:
[Notifier] [users-api:development] Створення користувача
[Notifier] Аудит: [users-api:development] Створення користувача
Обидва токени посилаються на той самий екземпляр: trueВикористовуйте useValue, якщо:
значення вже створене;
потрібно передати конфігурацію;
у тесті потрібна проста заміна залежності.
Використовуйте useClass, якщо:
потрібно вибрати конкретну реалізацію;
код має залежати від токена, а не від класу;
реалізація повинна створюватися контейнером NestJS.
Використовуйте useFactory, якщо:
значення потрібно обчислити;
для створення потрібні інші залежності;
створення залежності залежить від конфігурації;
ініціалізація є асинхронною.
Використовуйте useExisting, якщо:
для одного провайдера потрібен додатковий токен;
потрібно гарантувати використання того самого екземпляра;
різні частини застосунку використовують різні назви для однієї залежності.
exportsЯкщо провайдер потрібно використовувати в іншому модулі, його потрібно експортувати:
const APP_CONFIG = Symbol('APP_CONFIG');
@Module({
providers: [
{
provide: APP_CONFIG,
useValue: {
environment: 'production',
},
},
],
exports: [APP_CONFIG],
})
export class ConfigModule {}Після цього модуль-споживач має імпортувати ConfigModule:
@Module({
imports: [ConfigModule],
providers: [AppService],
})
export class UsersModule {}Експортувати потрібно саме токен, під яким зареєстровано провайдер:
exports: [APP_CONFIG]а не об’єкт, який було передано в useValue.
providersЯкщо токен використовується в @Inject(), але відповідного провайдера немає в модулі, NestJS не зможе створити залежність:
@Injectable()
export class AppService {
constructor(
@Inject('APP_CONFIG')
private readonly config: unknown,
) {}
}Для цього має існувати провайдер:
@Module({
providers: [
{
provide: 'APP_CONFIG',
useValue: {},
},
AppService,
],
})
export class AppModule {}Ці токени є різними:
Symbol('CONFIG');
Symbol('CONFIG');Навіть якщо вони мають однаковий опис, це два різні символи. Потрібно зберігати токен у змінній і використовувати саме її:
const CONFIG = Symbol('CONFIG');useClass і useExistinguseClass створює екземпляр указаного класу:
{
provide: 'PRIMARY',
useClass: ConsoleNotifier,
}useExisting використовує екземпляр іншого провайдера:
{
provide: 'PRIMARY',
useExisting: 'NOTIFIER',
}Для useExisting провайдер із токеном 'NOTIFIER' уже має бути зареєстрований.
Порядок у inject повинен відповідати параметрам фабрики:
{
provide: 'RESULT',
inject: ['CONFIG', 'LOGGER'],
useFactory: (config, logger) => {
// config відповідає CONFIG, logger відповідає LOGGER
return {};
},
}@InjectЯкщо провайдер зареєстрований рядком або символом, NestJS не зможе визначити його лише з типу TypeScript:
constructor(
private readonly config: AppConfig,
) {}Потрібно явно вказати токен:
constructor(
@Inject(APP_CONFIG)
private readonly config: AppConfig,
) {}Власний провайдер описується об’єктом із властивістю provide.
useValue передає готове значення.
useClass створює залежність через вказаний клас.
useFactory створює залежність результатом функції.
useExisting створює додатковий токен для вже наявного провайдера.
Для рядків і символів потрібно використовувати @Inject().
Залежності фабрики вказуються через inject.
useExisting повертає той самий екземпляр, а useClass створює окремий.
Провайдер, доступний в іншому модулі, потрібно додати до exports.