Пошук уроків, статей та іншого контенту
Створите асинхронні фабрики провайдерів для підключення конфігурації, баз даних і зовнішніх ресурсів.
Провайдер може створюватися не одразу, а після завершення асинхронної операції:
читання конфігурації з файлу або секрет-сховища;
встановлення з’єднання з базою даних;
створення клієнта зовнішнього сервісу;
отримання токена доступу;
перевірки доступності ресурсу.
Для цього у фабриці провайдера використовують async і повертають Promise:
{
provide: 'RESOURCE',
useFactory: async () => {
const resource = await createResource();
return resource;
},
}NestJS дочекається завершення фабрики під час створення модуля. Залежні провайдери отримають уже готове значення.
Розглянемо застосунок, якому потрібна конфігурація для підключення до бази даних і зовнішнього API.
Спочатку створимо провайдер конфігурації:
import { Provider } from '@nestjs/common';
export const APP_CONFIG = 'APP_CONFIG';
export interface AppConfig {
databaseUrl: string;
externalApiUrl: string;
}
export const appConfigProvider: Provider = {
provide: APP_CONFIG,
useFactory: async (): Promise<AppConfig> => {
// Імітуємо асинхронне читання конфігурації
await new Promise((resolve) => setTimeout(resolve, 100));
const databaseUrl = process.env.DATABASE_URL;
const externalApiUrl = process.env.EXTERNAL_API_URL;
if (!databaseUrl) {
throw new Error('Змінна DATABASE_URL не налаштована');
}
if (!externalApiUrl) {
throw new Error('Змінна EXTERNAL_API_URL не налаштована');
}
return {
databaseUrl,
externalApiUrl,
};
},
};Фабрика не має параметрів, тому для неї не потрібне поле inject.
Якщо змінна середовища відсутня, фабрика викине помилку, і NestJS не зможе запустити застосунок. Це краще, ніж запустити застосунок із некоректною конфігурацією та отримати помилку пізніше під час обробки запиту.
Фабрика також може отримувати інші провайдери через параметри. Для цього їхні токени потрібно вказати в inject.
import { Provider } from '@nestjs/common';
export const APP_CONFIG = 'APP_CONFIG';
export const API_CLIENT = 'API_CLIENT';
interface AppConfig {
externalApiUrl: string;
}
interface ApiClient {
getStatus(): Promise<unknown>;
}
export const apiClientProvider: Provider = {
provide: API_CLIENT,
inject: [APP_CONFIG],
useFactory: async (
config: AppConfig,
): Promise<ApiClient> => {
const response = await fetch(`${config.externalApiUrl}/status`);
if (!response.ok) {
throw new Error(
`Зовнішній API повернув статус ${response.status}`,
);
}
return {
async getStatus() {
const statusResponse = await fetch(
`${config.externalApiUrl}/status`,
);
return statusResponse.json();
},
};
},
};Порядок параметрів фабрики відповідає порядку токенів у inject:
inject: [CONFIG_A, CONFIG_B],
useFactory: async (configA, configB) => {
// configA відповідає CONFIG_A
// configB відповідає CONFIG_B
},NestJS спочатку створить APP_CONFIG, а потім передасть його фабриці API_CLIENT.
Асинхронний провайдер часто використовують для створення пулу з’єднань із базою даних.
Нижче наведено приклад із пакетом pg:
import { Provider } from '@nestjs/common';
import { Pool } from 'pg';
export const DATABASE_POOL = 'DATABASE_POOL';
interface AppConfig {
databaseUrl: string;
}
export const databaseProvider: Provider = {
provide: DATABASE_POOL,
inject: [APP_CONFIG],
useFactory: async (config: AppConfig): Promise<Pool> => {
const pool = new Pool({
connectionString: config.databaseUrl,
});
try {
// Перевіряємо з'єднання під час запуску застосунку
await pool.query('SELECT 1');
return pool;
} catch (error) {
// Закриваємо пул, якщо початкова перевірка неуспішна
await pool.end();
throw new Error(
`Не вдалося підключитися до бази даних: ${String(error)}`,
);
}
},
};Тепер Pool можна інжектити в сервіс за токеном DATABASE_POOL:
import { Inject, Injectable } from '@nestjs/common';
import { Pool } from 'pg';
@Injectable()
export class UsersRepository {
constructor(
@Inject(DATABASE_POOL)
private readonly database: Pool,
) {}
async findAll() {
const result = await this.database.query(
'SELECT id, email FROM users ORDER BY id',
);
return result.rows;
}
}Токен потрібен тому, що NestJS не може автоматично визначити, який саме тип потрібно створити для значення, що повертається фабрикою.
У наступному прикладі:
асинхронно створюється конфігурація;
на її основі створюється клієнт бази даних;
створюється клієнт зовнішнього API;
залежний сервіс використовує обидва ресурси.
import {
Controller,
Get,
Inject,
Injectable,
Module,
Provider,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Pool } from 'pg';
export const APP_CONFIG = 'APP_CONFIG';
export const DATABASE_POOL = 'DATABASE_POOL';
export const EXTERNAL_API_CLIENT = 'EXTERNAL_API_CLIENT';
interface AppConfig {
databaseUrl: string;
externalApiUrl: string;
}
interface ExternalApiClient {
getStatus(): Promise<unknown>;
}
const appConfigProvider: Provider = {
provide: APP_CONFIG,
useFactory: async (): Promise<AppConfig> => {
// Імітуємо асинхронне завантаження конфігурації
await new Promise((resolve) => setTimeout(resolve, 100));
const databaseUrl = process.env.DATABASE_URL;
const externalApiUrl = process.env.EXTERNAL_API_URL;
if (!databaseUrl) {
throw new Error('Змінна DATABASE_URL не налаштована');
}
if (!externalApiUrl) {
throw new Error('Змінна EXTERNAL_API_URL не налаштована');
}
return {
databaseUrl,
externalApiUrl,
};
},
};
const databaseProvider: Provider = {
provide: DATABASE_POOL,
inject: [APP_CONFIG],
useFactory: async (config: AppConfig): Promise<Pool> => {
const pool = new Pool({
connectionString: config.databaseUrl,
});
try {
// Перевіряємо базу даних під час запуску
await pool.query('SELECT 1');
return pool;
} catch (error) {
await pool.end();
throw new Error(
`Помилка підключення до бази даних: ${String(error)}`,
);
}
},
};
const externalApiProvider: Provider = {
provide: EXTERNAL_API_CLIENT,
inject: [APP_CONFIG],
useFactory: async (
config: AppConfig,
): Promise<ExternalApiClient> => {
const response = await fetch(`${config.externalApiUrl}/status`);
if (!response.ok) {
throw new Error(
`Зовнішній API недоступний: HTTP ${response.status}`,
);
}
return {
async getStatus() {
const statusResponse = await fetch(
`${config.externalApiUrl}/status`,
);
if (!statusResponse.ok) {
throw new Error(
`Помилка зовнішнього API: HTTP ${statusResponse.status}`,
);
}
return statusResponse.json();
},
};
},
};
@Injectable()
class HealthService {
constructor(
@Inject(DATABASE_POOL)
private readonly database: Pool,
@Inject(EXTERNAL_API_CLIENT)
private readonly externalApi: ExternalApiClient,
) {}
async check() {
const databaseResult = await this.database.query('SELECT 1');
const externalApiStatus = await this.externalApi.getStatus();
return {
database: databaseResult.rowCount === 1 ? 'up' : 'down',
externalApi: externalApiStatus,
};
}
}
@Controller('health')
class HealthController {
constructor(private readonly healthService: HealthService) {}
@Get()
check() {
return this.healthService.check();
}
}
@Module({
providers: [
appConfigProvider,
databaseProvider,
externalApiProvider,
HealthService,
],
controllers: [HealthController],
})
class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Для запуску прикладу потрібні:
встановлений пакет pg;
доступна база даних PostgreSQL;
змінна DATABASE_URL;
змінна EXTERNAL_API_URL, яка вказує на API з endpoint /status.
Якщо будь-яка асинхронна фабрика завершиться помилкою, NestFactory.create() також завершиться помилкою, а сервер не почне приймати запити.
Провайдери потрібно зареєструвати в providers модуля:
@Module({
providers: [
appConfigProvider,
databaseProvider,
externalApiProvider,
],
exports: [
APP_CONFIG,
DATABASE_POOL,
EXTERNAL_API_CLIENT,
],
})
export class InfrastructureModule {}Якщо провайдер потрібен в іншому модулі, його потрібно:
додати до providers;
додати його токен до exports;
імпортувати модуль-джерело.
@Module({
imports: [InfrastructureModule],
providers: [UsersRepository],
})
export class UsersModule {}Не потрібно повторно створювати такий самий провайдер у кожному модулі. Якщо провайдер експортується модулем, залежні модулі можуть використовувати вже створений екземпляр.
useClass і useExistingВластивість useFactory зручна, коли логіку створення ресурсу можна описати безпосередньо в модулі. Для складнішої логіки можна використовувати клас із методом create....
Наприклад, окремий клас може створювати конфігурацію:
import { Injectable } from '@nestjs/common';
@Injectable()
export class ConfigFactory {
async createConfig(): Promise<AppConfig> {
// Конфігурація може завантажуватися з файлу або секрет-сховища
return {
databaseUrl: process.env.DATABASE_URL ?? '',
externalApiUrl: process.env.EXTERNAL_API_URL ?? '',
};
}
}Провайдер useClass сам створить екземпляр цього класу:
const configProvider: Provider = {
provide: APP_CONFIG,
useFactory: async (factory: ConfigFactory) => {
return factory.createConfig();
},
inject: [ConfigFactory],
};Якщо ConfigFactory уже зареєстрований в іншому модулі, для повторного використання того самого екземпляра можна застосувати useExisting у конфігураціях модулів, які підтримують асинхронний варіант налаштування.
Загальна ідея така:
useFactory — створити значення функцією;
useClass — створити окремий клас для фабрики;
useExisting — використати вже зареєстрований екземпляр фабрики.
Для власних провайдерів найчастіше достатньо useFactory.
NestJS будує граф залежностей на основі inject.
Наприклад:
const firstProvider = {
provide: 'FIRST',
useFactory: async () => {
return 'first';
},
};
const secondProvider = {
provide: 'SECOND',
inject: ['FIRST'],
useFactory: async (first: string) => {
return `${first}-second`;
},
};У цьому випадку:
створюється FIRST;
NestJS очікує завершення його фабрики;
значення передається в SECOND;
створюється SECOND.
Завдяки цьому не потрібно вручну керувати порядком викликів фабрик.
awaitНеправильно:
useFactory: () => createDatabaseConnection(),Такий код може бути коректним, якщо createDatabaseConnection() повертає Promise, адже NestJS очікує асинхронні значення. Проте під час складнішої логіки часто помилково повертають проміжний результат:
useFactory: async () => {
const connection = createDatabaseConnection();
return connection.query('SELECT 1');
},У цьому випадку фабрика поверне результат запиту, а не саме з’єднання.
Правильно:
useFactory: async () => {
const connection = await createDatabaseConnection();
await connection.query('SELECT 1');
return connection;
},injectНеправильно:
const provider = {
provide: 'DATABASE',
useFactory: async (config: AppConfig) => {
return createDatabase(config.databaseUrl);
},
};NestJS не знає, що потрібно передати в config.
Правильно:
const provider = {
provide: 'DATABASE',
inject: [APP_CONFIG],
useFactory: async (config: AppConfig) => {
return createDatabase(config.databaseUrl);
},
};inject: ['CONFIG', 'LOGGER'],
useFactory: async (logger, config) => {
// Параметри переплутані
},Потрібно дотримуватися однакового порядку:
inject: ['CONFIG', 'LOGGER'],
useFactory: async (config, logger) => {
// config відповідає CONFIG
// logger відповідає LOGGER
},Асинхронний провайдер за замовчуванням має singleton-область видимості. Його фабрика виконується під час створення модуля, а не для кожного HTTP-запиту.
Не слід створювати підключення до бази даних усередині контролера:
@Get()
async getUsers() {
const database = await createDatabaseConnection();
return database.query('SELECT * FROM users');
}Підключення або пул потрібно створити провайдером і потім інжектити в сервіси.
Якщо фабрика просто повертає клієнт, але не перевіряє його, помилка може з’явитися лише під час першого запиту.
Для критичних ресурсів бажано перевірити підключення у фабриці:
useFactory: async () => {
const client = await createClient();
await client.ping();
return client;
},Не варто повертати некоректний ресурс після помилки:
useFactory: async () => {
try {
return await connect();
} catch (error) {
console.error(error);
return null;
}
},У результаті залежні сервіси отримають null і помилка виникне в іншому місці. Краще повторно викинути помилку, щоб застосунок не запускався в непрацездатному стані:
useFactory: async () => {
try {
return await connect();
} catch (error) {
throw new Error(`Підключення неуспішне: ${String(error)}`);
}
},Асинхронний провайдер створюється через useFactory з async-функцією.
NestJS очікує завершення Promise перед створенням залежних провайдерів.
Залежності фабрики передаються через inject.
Конфігурацію, підключення до бази даних і клієнти зовнішніх сервісів зручно створювати асинхронними провайдерами.
Критичні ресурси варто перевіряти під час запуску застосунку.
Для спільного використання провайдер потрібно експортувати з модуля.
У разі помилки фабрики краще зупинити запуск застосунку, ніж продовжити роботу з некоректним ресурсом.