Custom Providers: useValue, useClass, useFactory, useExisting
Custom Providers: useValue, useClass, useFactory, useExisting
Короткий зміст
- Стандартний синтаксис провайдера: shorthand notation
useValue: надання готового значення (константи, конфігурації)useClass: явне вказування класу реалізаціїuseFactory: динамічне створення провайдера через фабричну функціюuseExisting: створення псевдонімів (alias) для існуючих провайдерів- Injection tokens: коли ім'я класу недостатньо
- Асинхронні фабрики: async useFactory для ініціалізації з'єднань
- Залежності фабрики: параметр
inject - Практичні сценарії: підключення до БД, завантаження конфігурації
- Вибір правильного типу провайдера для конкретної задачі
Стандартний синтаксис провайдера: shorthand notation
У попередніх лекціях ми реєстрували провайдери у модулях, використовуючи скорочений синтаксис (shorthand notation) — просто передаючи клас у масив providers:
@Module({
providers: [UsersService, EmailService, LoggerService]
})
export class UsersModule {}
Цей лаконічний синтаксис є насправді скороченням для більш детального запису:
@Module({
providers: [
{
provide: UsersService,
useClass: UsersService
},
{
provide: EmailService,
useClass: EmailService
},
{
provide: LoggerService,
useClass: LoggerService
}
]
})
export class UsersModule {}
Кожен провайдер насправді є об'єктом з кількома властивостями:
provide— токен (token), який ідентифікує провайдер у DI-контейнері. Це те, що використовується у параметрах конструктора для запиту залежності.useClass— клас, екземпляр якого буде створено та ін'єктовано при запиті цього токена.
Коли ви пишете просто UsersService у масиві providers, NestJS автоматично розгортає це у { provide: UsersService, useClass: UsersService } — тобто використовує клас одночасно як токен та як реалізацію.
Проте NestJS надає кілька альтернативних стратегій реєстрації провайдерів через властивості useValue, useFactory та useExisting, які дозволяють гнучко налаштовувати створення та надання залежностей.
🎯 Мета лекції
- Зрозуміти різницю між shorthand та повним синтаксисом провайдерів
- Опанувати чотири стратегії реєстрації:
useValue,useClass,useFactory,useExisting - Навчитися створювати провайдери для значень, що не є класами
- Практикувати використання фабричних функцій для динамічного створення провайдерів
- Розібрати асинхронну ініціалізацію через
useFactory - Застосовувати правильну стратегію для конкретних сценаріїв
🔑 Ключові терміни
- Provider Token — ідентифікатор провайдера у DI-контейнері (клас, рядок або Symbol)
- useValue — надання готового значення як провайдера
- useClass — явне вказування класу для створення екземпляру
- useFactory — динамічне створення провайдера через функцію
- useExisting — створення псевдоніма (alias) для існуючого провайдера
useValue: надання готового значення
Стратегія useValue дозволяє зареєструвати будь-яке готове значення як провайдер: об'єкт конфігурації, константу, примітивне значення, масив, функцію тощо. Це корисно, коли вам не потрібно створювати новий екземпляр класу, а достатньо надати існуюче значення.
Базовий приклад: об'єкт конфігурації
// config.constants.ts
export const APP_CONFIG = {
apiUrl: 'https://api.example.com',
apiKey: 'secret_key_12345',
timeout: 5000,
retryAttempts: 3
};
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_CONFIG } from './config.constants';
@Module({
providers: [
{
provide: 'APP_CONFIG',
useValue: APP_CONFIG
}
]
})
export class AppModule {}
Тепер цей об'єкт конфігурації можна ін'єктувати у будь-який сервіс через декоратор @Inject():
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class ApiService {
constructor(
@Inject('APP_CONFIG') private readonly config: typeof APP_CONFIG
) {
console.log(`API URL: ${this.config.apiUrl}`);
}
async fetchData() {
const response = await fetch(this.config.apiUrl, {
headers: {
'Authorization': `Bearer ${this.config.apiKey}`
},
signal: AbortSignal.timeout(this.config.timeout)
});
return response.json();
}
}
Зверніть увагу на декоратор @Inject('APP_CONFIG') — він явно вказує DI-контейнеру, який токен потрібно ін'єктувати. Оскільки APP_CONFIG не є класом, TypeScript не може автоматично визначити тип параметра конструктора, тому ми використовуємо @Inject() для явного зв'язування.
Приклади використання useValue
1. Константи та прапорці:
@Module({
providers: [
{
provide: 'IS_PRODUCTION',
useValue: process.env.NODE_ENV === 'production'
},
{
provide: 'MAX_UPLOAD_SIZE',
useValue: 10 * 1024 * 1024 // 10 MB
}
]
})
export class AppModule {}
2. Масиви допустимих значень:
@Module({
providers: [
{
provide: 'ALLOWED_ORIGINS',
useValue: ['http://localhost:3000', 'https://example.com']
}
]
})
export class AppModule {}
3. Мок-об'єкти для тестування:
// app.module.spec.ts
const mockEmailService = {
sendEmail: jest.fn().mockResolvedValue(true)
};
Test.createTestingModule({
providers: [
UsersService,
{
provide: EmailService,
useValue: mockEmailService // Підміна реального сервісу на mock
}
]
});
useValue для надання незмінних конфігурацій, констант та готових об'єктів. Це особливо корисно для тестування, коли потрібно швидко замінити реальні залежності на мок-об'єкти без створення окремих класів.useClass: явне вказування класу реалізації
Стратегія useClass дозволяє явно вказати, який клас має бути створений та ін'єктований при запиті певного токена. Це корисно у кількох сценаріях:
- Абстрактні класи та інтерфейси — коли токен (абстракція) відрізняється від реалізації
- Підміна реалізації залежно від середовища (розробка/продакшн)
- Стратегія паттерн — вибір конкретної реалізації на основі конфігурації
Приклад: абстрактний клас та різні реалізації
// logger.interface.ts
export abstract class Logger {
abstract log(message: string): void;
abstract error(message: string, trace?: string): void;
}
// console-logger.service.ts
import { Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';
@Injectable()
export class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
error(message: string, trace?: string): void {
console.error(`[ERROR] ${message}`);
if (trace) console.error(trace);
}
}
// file-logger.service.ts
import { Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';
import * as fs from 'fs';
@Injectable()
export class FileLogger implements Logger {
private readonly logFile = 'app.log';
log(message: string): void {
const timestamp = new Date().toISOString();
fs.appendFileSync(this.logFile, `[${timestamp}] [LOG] ${message}\n`);
}
error(message: string, trace?: string): void {
const timestamp = new Date().toISOString();
fs.appendFileSync(this.logFile, `[${timestamp}] [ERROR] ${message}\n`);
if (trace) fs.appendFileSync(this.logFile, `${trace}\n`);
}
}
Тепер ми можемо зареєструвати різні реалізації залежно від середовища:
// app.module.ts
import { Module } from '@nestjs/common';
import { Logger } from './logger.interface';
import { ConsoleLogger } from './console-logger.service';
import { FileLogger } from './file-logger.service';
const isDevelopment = process.env.NODE_ENV === 'development';
@Module({
providers: [
{
provide: Logger,
useClass: isDevelopment ? ConsoleLogger : FileLogger
}
]
})
export class AppModule {}
Тепер у всіх сервісах, які залежать від Logger, буде автоматично ін'єктована правильна реалізація:
@Injectable()
export class UsersService {
constructor(private readonly logger: Logger) {
this.logger.log('UsersService ініціалізовано');
}
async create(userData: any) {
this.logger.log(`Створення користувача: ${userData.email}`);
// ...
}
}
Порівняння shorthand та useClass
@Module({
providers: [UsersService]
})
export class UsersModule {}
@Module({
providers: [
{
provide: UsersService,
useClass: UsersService
}
]
})
export class UsersModule {}
@Module({
providers: [
{
provide: Logger, // Токен (абстракція)
useClass: ConsoleLogger // Реалізація (конкретний клас)
}
]
})
export class UsersModule {}
provide: UsersService, useClass: UsersService), використовуйте shorthand-форму для лаконічності. Явний синтаксис useClass потрібен лише тоді, коли токен відрізняється від класу реалізації.useFactory: динамічне створення провайдера через фабричну функцію
Стратегія useFactory є найпотужнішою з усіх — вона дозволяє динамічно створювати провайдери через фабричну функцію (factory function). Це особливо корисно, коли:
- Створення провайдера вимагає складної логіки
- Провайдер залежить від змінних оточення або конфігурації
- Потрібна асинхронна ініціалізація (підключення до БД, завантаження конфігурації з файлу)
- Провайдер має бути створений на основі інших провайдерів
Базовий приклад: створення HTTP-клієнта з налаштуваннями
import { Module } from '@nestjs/common';
import axios, { AxiosInstance } from 'axios';
@Module({
providers: [
{
provide: 'HTTP_CLIENT',
useFactory: (): AxiosInstance => {
return axios.create({
baseURL: process.env.API_URL || 'https://api.example.com',
timeout: parseInt(process.env.API_TIMEOUT || '5000'),
headers: {
'User-Agent': 'NestJS-App/1.0',
'Accept': 'application/json'
}
});
}
}
]
})
export class AppModule {}
Фабрична функція викликається один раз при ініціалізації модуля, і її результат зберігається у DI-контейнері як singleton.
Фабрика із залежностями: параметр inject
Фабричні функції можуть залежати від інших провайдерів через параметр inject — масив токенів, які DI-контейнер передасть як аргументи фабричної функції:
// config.service.ts
@Injectable()
export class ConfigService {
get(key: string): string {
return process.env[key] || '';
}
getNumber(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseInt(value) : defaultValue;
}
}
// app.module.ts
@Module({
providers: [
ConfigService,
{
provide: 'HTTP_CLIENT',
useFactory: (config: ConfigService): AxiosInstance => {
return axios.create({
baseURL: config.get('API_URL'),
timeout: config.getNumber('API_TIMEOUT', 5000),
headers: {
'Authorization': `Bearer ${config.get('API_KEY')}`
}
});
},
inject: [ConfigService] // Залежності фабрики
}
]
})
export class AppModule {}
DI-контейнер автоматично розв'язує залежності, перераховані у масиві inject, та передає їх екземпляри як параметри фабричної функції у тому самому порядку.
Складний приклад: фабрика з кількома залежностями
@Module({
providers: [
ConfigService,
LoggerService,
{
provide: 'DATABASE_CONNECTION',
useFactory: async (
config: ConfigService,
logger: LoggerService
): Promise<DataSource> => {
logger.log('Ініціалізація підключення до БД', 'DatabaseFactory');
const dataSource = new DataSource({
type: 'postgres',
host: config.get('DB_HOST'),
port: config.getNumber('DB_PORT', 5432),
username: config.get('DB_USERNAME'),
password: config.get('DB_PASSWORD'),
database: config.get('DB_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: config.get('NODE_ENV') === 'development'
});
try {
await dataSource.initialize();
logger.log('Підключення до БД успішне', 'DatabaseFactory');
return dataSource;
} catch (error) {
logger.error('Помилка підключення до БД', error.stack, 'DatabaseFactory');
throw error;
}
},
inject: [ConfigService, LoggerService]
}
]
})
export class DatabaseModule {}
Цей приклад демонструє кілька важливих аспектів:
- Асинхронна фабрика: функція позначена як
asyncта повертаєPromise - Множинні залежності: фабрика залежить від
ConfigServiceтаLoggerService - Обробка помилок: логування успіху/помилки підключення
- Динамічна конфігурація: параметри підключення завантажуються зі змінних оточення
Асинхронні фабрики: async useFactory для ініціалізації з'єднань
Асинхронні фабрики (async factories) дозволяють виконувати операції введення-виведення під час ініціалізації провайдера: підключення до бази даних, завантаження конфігурації з віддаленого сервера, ініціалізація кешу тощо. NestJS автоматично чекає на завершення всіх асинхронних фабрик перед запуском застосунку.
Приклад: завантаження конфігурації з файлу
import { Module } from '@nestjs/common';
import * as fs from 'fs/promises';
import * as path from 'path';
interface AppConfig {
database: {
host: string;
port: number;
};
redis: {
host: string;
port: number;
};
features: {
enableCaching: boolean;
enableMetrics: boolean;
};
}
@Module({
providers: [
{
provide: 'APP_CONFIG',
useFactory: async (): Promise<AppConfig> => {
const configPath = path.join(process.cwd(), 'config', 'app.config.json');
try {
const configData = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(configData);
console.log('Конфігурацію успішно завантажено');
return config;
} catch (error) {
console.error('Помилка завантаження конфігурації, використовуємо значення за замовчуванням');
// Конфігурація за замовчуванням
return {
database: {
host: 'localhost',
port: 5432
},
redis: {
host: 'localhost',
port: 6379
},
features: {
enableCaching: true,
enableMetrics: false
}
};
}
}
}
]
})
export class ConfigModule {}
Приклад: ініціалізація Redis-підключення
import { Module } from '@nestjs/common';
import { createClient, RedisClientType } from 'redis';
@Module({
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: async (): Promise<RedisClientType> => {
const client = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
client.on('error', (err) => {
console.error('Redis Client Error', err);
});
client.on('connect', () => {
console.log('✓ Підключено до Redis');
});
await client.connect();
// Перевірка з'єднання
await client.ping();
return client;
}
}
],
exports: ['REDIS_CLIENT']
})
export class RedisModule {}
Тепер Redis-клієнт можна ін'єктувати у сервіси:
import { Injectable, Inject } from '@nestjs/common';
import { RedisClientType } from 'redis';
@Injectable()
export class CacheService {
constructor(
@Inject('REDIS_CLIENT') private readonly redis: RedisClientType
) {}
async set(key: string, value: string, ttl: number = 3600): Promise<void> {
await this.redis.setEx(key, ttl, value);
}
async get(key: string): Promise<string | null> {
return this.redis.get(key);
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
}
useExisting: створення псевдонімів (alias) для існуючих провайдерів
Стратегія useExisting дозволяє створювати псевдоніми (aliases) для існуючих провайдерів — кілька токенів, які посилаються на один і той самий екземпляр провайдера. Це корисно у кількох сценаріях:
- Забезпечення зворотної сумісності при рефакторингу (старий токен + новий токен)
- Підтримка кількох назв для одного провайдера
- Створення загальних абстракцій (наприклад,
Loggerяк псевдонім дляConsoleLogger)
Базовий приклад: псевдонім для сервісу
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Module({
providers: [
LoggerService,
{
provide: 'Logger', // Рядковий токен
useExisting: LoggerService // Посилання на існуючий провайдер
}
]
})
export class AppModule {}
Тепер обидва способи ін'єкції посилаються на той самий екземпляр:
@Injectable()
export class UsersService {
constructor(
private readonly loggerByClass: LoggerService,
@Inject('Logger') private readonly loggerByToken: any
) {
console.log(loggerByClass === loggerByToken); // true — це той самий об'єкт
}
}
Приклад: рефакторинг зі збереженням зворотної сумісності
Припустімо, ви перейменували OldEmailService на EmailService, але хочете, щоб старий код продовжував працювати:
import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
@Module({
providers: [
EmailService,
{
provide: 'OldEmailService', // Старий токен для зворотної сумісності
useExisting: EmailService // Посилається на новий сервіс
}
]
})
export class EmailModule {}
Тепер обидва варіанти працюватимуть:
// Новий код
constructor(private readonly emailService: EmailService) {}
// Старий код (продовжує працювати)
constructor(@Inject('OldEmailService') private readonly emailService: any) {}
Відмінність useExisting від useClass
Важливо розуміти різницю між useExisting та useClass:
@Module({
providers: [
LoggerService,
{
provide: 'Logger',
useExisting: LoggerService // Псевдонім для існуючого екземпляру
}
]
})
export class AppModule {}
// Результат:
// - LoggerService створюється один раз
// - 'Logger' посилається на той самий екземпляр
@Module({
providers: [
LoggerService,
{
provide: 'Logger',
useClass: LoggerService // Створення нового екземпляру
}
]
})
export class AppModule {}
// Результат:
// - LoggerService створюється один раз
// - 'Logger' створює НОВИЙ екземпляр LoggerService
// - Всього два різні екземпляри
useExisting створює псевдонім для існуючого singleton-екземпляру, тоді як useClass створює новий незалежний екземпляр.
useExisting, коли вам потрібно, щоб кілька токенів посилалися на один і той самий екземпляр (наприклад, для зворотної сумісності або спільного стану). Використовуйте useClass, коли потрібні незалежні екземпляри з власним станом.Injection tokens: коли ім'я класу недостатньо
У всіх попередніх прикладах із useValue та useFactory ми використовували рядкові токени (string tokens) на кшталт 'APP_CONFIG', 'HTTP_CLIENT', 'REDIS_CLIENT'. Це необхідно, оскільки провайдери створені через useValue або useFactory часто не мають відповідного класу TypeScript, який міг би слугувати токеном.
Проблема: константи та примітиви не є класами
Розглянемо проблемну ситуацію:
// ❌ НЕ ПРАЦЮЄ: неможливо використовувати об'єкт як токен
const config = { apiUrl: 'https://api.example.com' };
@Module({
providers: [
{
provide: config, // Помилка: об'єкт не може бути токеном
useValue: config
}
]
})
export class AppModule {}
Токеном може бути лише:
- Клас (найпоширеніший варіант)
- Рядок (для значень, що не є класами)
- Symbol (для унікальності та уникнення колізій)
Рішення: рядкові токени
// ✅ ПРАЦЮЄ: рядок як токен
@Module({
providers: [
{
provide: 'APP_CONFIG',
useValue: { apiUrl: 'https://api.example.com' }
}
]
})
export class AppModule {}
Ін'єкція через декоратор @Inject():
@Injectable()
export class ApiService {
constructor(
@Inject('APP_CONFIG') private readonly config: { apiUrl: string }
) {}
}
Винесення токенів у константи
Для уникнення друкарських помилок та забезпечення централізованого управління токенами рекомендується виносити їх у окремий файл:
// tokens.constants.ts
export const TOKENS = {
APP_CONFIG: 'APP_CONFIG',
HTTP_CLIENT: 'HTTP_CLIENT',
DATABASE_CONNECTION: 'DATABASE_CONNECTION',
REDIS_CLIENT: 'REDIS_CLIENT',
LOGGER: 'LOGGER'
} as const;
// app.module.ts
import { TOKENS } from './tokens.constants';
@Module({
providers: [
{
provide: TOKENS.APP_CONFIG,
useValue: { /* ... */ }
}
]
})
export class AppModule {}
// api.service.ts
import { TOKENS } from './tokens.constants';
@Injectable()
export class ApiService {
constructor(
@Inject(TOKENS.APP_CONFIG) private readonly config: any
) {}
}
Це забезпечує:
- Безпеку типів: якщо токен не існує, TypeScript покаже помилку на етапі компіляції
- Автодоповнення: IDE підказуватиме доступні токени
- Легкість рефакторингу: зміна токена в одному місці оновить всі використання
Symbol замість рядків для уникнення колізій імен та створення типобезпечних токенів через інтерфейси.Практичні сценарії використання custom providers
Розглянемо кілька реальних сценаріїв, де custom providers є ідеальним рішенням:
Сценарій 1: Підключення до MongoDB
import { Module } from '@nestjs/common';
import { MongoClient, Db } from 'mongodb';
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (): Promise<Db> => {
const uri = process.env.MONGO_URI || 'mongodb://localhost:27017';
const client = new MongoClient(uri);
await client.connect();
console.log('✓ Підключено до MongoDB');
return client.db(process.env.MONGO_DB_NAME || 'myapp');
}
}
],
exports: ['DATABASE_CONNECTION']
})
export class DatabaseModule {}
Сценарій 2: HTTP-клієнт з автентифікацією
@Module({
providers: [
ConfigService,
{
provide: 'AUTHENTICATED_HTTP_CLIENT',
useFactory: async (config: ConfigService): Promise<AxiosInstance> => {
// Отримання токена з API автентифікації
const authResponse = await axios.post(
config.get('AUTH_URL'),
{
clientId: config.get('CLIENT_ID'),
clientSecret: config.get('CLIENT_SECRET')
}
);
const token = authResponse.data.access_token;
// Створення клієнта з автентифікацією
return axios.create({
baseURL: config.get('API_URL'),
headers: {
'Authorization': `Bearer ${token}`
}
});
},
inject: [ConfigService]
}
]
})
export class HttpModule {}
Сценарій 3: Різні реалізації сервісу для різних середовищ
import { Module } from '@nestjs/common';
import { StorageService } from './storage.interface';
import { LocalStorageService } from './local-storage.service';
import { S3StorageService } from './s3-storage.service';
@Module({
providers: [
LocalStorageService,
S3StorageService,
{
provide: StorageService,
useFactory: (
local: LocalStorageService,
s3: S3StorageService
): StorageService => {
const useS3 = process.env.STORAGE_TYPE === 's3';
return useS3 ? s3 : local;
},
inject: [LocalStorageService, S3StorageService]
}
]
})
export class StorageModule {}
Сценарій 4: Кеш-менеджер з TTL за замовчуванням
@Module({
providers: [
{
provide: 'CACHE_MANAGER',
useValue: {
cache: new Map<string, { value: any; expires: number }>(),
set(key: string, value: any, ttl: number = 300): void {
this.cache.set(key, {
value,
expires: Date.now() + ttl * 1000
});
},
get(key: string): any | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.value;
},
delete(key: string): void {
this.cache.delete(key);
}
}
}
]
})
export class CacheModule {}
Вибір правильного типу провайдера для конкретної задачі
Розуміння того, яку стратегію реєстрації провайдера використовувати у конкретному сценарії, є важливою навичкою. Розглянемо порівняльну таблицю та рекомендації:
Таблиця рекомендацій
| Сценарій | Стратегія | Приклад |
|---|---|---|
Звичайний сервіс з @Injectable | Shorthand | providers: [UsersService] |
| Абстрактний клас + конкретна реалізація | useClass | { provide: Logger, useClass: ConsoleLogger } |
| Константа або конфігурація | useValue | { provide: 'API_KEY', useValue: 'secret' } |
| Мок-об'єкт для тестування | useValue | { provide: EmailService, useValue: mockEmail } |
| Підключення до БД | useFactory (async) | useFactory: async () => await createConnection() |
| HTTP-клієнт з налаштуваннями | useFactory | useFactory: () => axios.create({ ... }) |
| Динамічний вибір реалізації | useFactory | useFactory: () => isDev ? DevService : ProdService |
| Псевдонім для існуючого провайдера | useExisting | { provide: 'Logger', useExisting: LoggerService } |
| Зворотна сумісність після рефакторингу | useExisting | { provide: OldName, useExisting: NewName } |
Контрольний список для вибору стратегії
Крок 1: Визначте тип значення
Чи є провайдер класом з декоратором @Injectable? Якщо так, використовуйте shorthand або useClass.
Крок 2: Перевірте необхідність динаміки
Чи потрібна логіка для створення провайдера (умови, залежності, асинхронність)? Якщо так, використовуйте useFactory.
Крок 3: Оцініть складність
Чи є це простий об'єкт, константа або готове значення? Якщо так, використовуйте useValue.
Крок 4: Перевірте унікальність
Чи потрібен псевдонім для існуючого провайдера? Якщо так, використовуйте useExisting.
Так, абсолютно нормально реєструвати провайдери з різними стратегіями у одному модулі:
@Module({
providers: [
UsersService, // shorthand
{ provide: Logger, useClass: ConsoleLogger }, // useClass
{ provide: 'API_KEY', useValue: 'secret' }, // useValue
{ provide: 'DB', useFactory: createDB } // useFactory
]
})
Так, NestJS повністю підтримує асинхронні фабрики. Просто поверніть Promise з фабричної функції, і DI-контейнер автоматично дочекається її завершення перед запуском застосунку:
{
provide: 'DB',
useFactory: async () => {
const connection = await createConnection();
return connection;
}
}
Вкажіть тип у сигнатурі фабричної функції:
{
provide: 'HTTP_CLIENT',
useFactory: (): AxiosInstance => {
return axios.create({ /* ... */ });
}
}
При ін'єкції використовуйте цей тип:
constructor(
@Inject('HTTP_CLIENT') private readonly http: AxiosInstance
) {}
Порівняння всіх чотирьох стратегій: практичний приклад
Для закріплення матеріалу розглянемо модуль, що використовує всі чотири стратегії одночасно:
// logger.interface.ts
export abstract class Logger {
abstract log(message: string): void;
abstract error(message: string): void;
}
// console-logger.service.ts
@Injectable()
export class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${message}`);
}
}
// config.service.ts
@Injectable()
export class ConfigService {
get(key: string): string {
return process.env[key] || '';
}
}
// app.module.ts
import { Module } from '@nestjs/common';
import { Logger } from './logger.interface';
import { ConsoleLogger } from './console-logger.service';
import { ConfigService } from './config.service';
import axios from 'axios';
@Module({
providers: [
// 1. Shorthand: звичайний сервіс
ConfigService,
// 2. useClass: абстрактний клас + реалізація
{
provide: Logger,
useClass: ConsoleLogger
},
// 3. useValue: константа конфігурації
{
provide: 'APP_NAME',
useValue: 'MyNestJSApp'
},
// 4. useFactory: HTTP-клієнт з залежностями
{
provide: 'HTTP_CLIENT',
useFactory: (config: ConfigService) => {
return axios.create({
baseURL: config.get('API_URL'),
timeout: 5000
});
},
inject: [ConfigService]
},
// 5. useExisting: псевдонім для логера
{
provide: 'LOGGER',
useExisting: Logger
}
]
})
export class AppModule {}
Тепер усі провайдери можна ін'єктувати у сервіс:
import { Injectable, Inject } from '@nestjs/common';
import { Logger } from './logger.interface';
import { ConfigService } from './config.service';
import { AxiosInstance } from 'axios';
@Injectable()
export class ApiService {
constructor(
// Shorthand: автоматична ін'єкція за типом
private readonly config: ConfigService,
// useClass: ін'єкція за абстрактним класом
private readonly logger: Logger,
// useValue: ін'єкція константи
@Inject('APP_NAME') private readonly appName: string,
// useFactory: ін'єкція створеного об'єкта
@Inject('HTTP_CLIENT') private readonly http: AxiosInstance,
// useExisting: псевдонім (той самий екземпляр, що і Logger)
@Inject('LOGGER') private readonly loggerAlias: Logger
) {
this.logger.log(`${this.appName} ініціалізовано`);
console.log(this.logger === this.loggerAlias); // true — той самий об'єкт
}
async fetchData() {
try {
const response = await this.http.get('/data');
this.logger.log('Дані успішно отримано');
return response.data;
} catch (error) {
this.logger.error(`Помилка отримання даних: ${error.message}`);
throw error;
}
}
}
Резюме
Custom providers надають гнучкі механізми для реєстрації та налаштування залежностей у NestJS. Ключові тези лекції:
✅ Чотири стратегії
- Shorthand:
[UsersService]— для класів з@Injectable - useClass: явна реалізація для абстракцій
- useValue: готові значення, константи, мок-об'єкти
- useFactory: динамічне створення з логікою та залежностями
- useExisting: псевдоніми для існуючих провайдерів
🎯 Коли використовувати
- useClass: різні реалізації залежно від середовища
- useValue: конфігурації, константи, тестові моки
- useFactory: підключення до БД, HTTP-клієнти, асинхронна ініціалізація
- useExisting: зворотна сумісність, псевдоніми
У наступній лекції ми детальніше розглянемо Injection Tokens — використання рядкових та символьних токенів, декоратор @Inject(), створення типобезпечних токенів та найкращі практики їх організації у великих застосунках.