Пошук уроків, статей та іншого контенту
Реалізуєте модуль із динамічною конфігурацією через forRoot та forFeature.
Звичайний модуль має фіксовану конфігурацію:
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}Динамічний модуль формує частину метаданих під час виклику статичного методу. Такий метод повертає об’єкт типу DynamicModule:
{
module: SomeModule,
providers: [...],
imports: [...],
exports: [...],
}Це дає змогу передавати модулю конфігурацію під час імпорту:
@Module({
imports: [
DatabaseModule.forRoot({
databaseName: 'main',
}),
],
})
export class AppModule {}У результаті модуль може:
створити провайдери на основі конфігурації;
зареєструвати різні набори провайдерів у різних функціональних модулях;
приховати внутрішню реалізацію підключення;
надати зручний API на кшталт forRoot() і forFeature().
forRoot та forFeatureУ NestJS ці методи не мають спеціального вбудованого значення. Це домовленість, яка стала стандартом для динамічних модулів.
forRootforRoot() зазвичай викликається один раз у кореневому модулі застосунку.
Він призначений для глобальної конфігурації:
створення підключення до бази даних;
налаштування клієнта зовнішнього API;
реєстрації загальних параметрів;
створення спільного singleton-провайдера.
Приклад:
DatabaseModule.forRoot({
databaseName: 'main',
});forFeatureforFeature() зазвичай викликається у функціональних модулях.
Він реєструє ресурси, потрібні лише конкретному модулю:
репозиторії сутностей;
клієнти окремих API;
обробники певного типу повідомлень;
провайдери, пов’язані з конкретною функціональністю.
Приклад:
DatabaseModule.forFeature([User, Order])forRoot() створює спільний ресурс, а forFeature() використовує його для створення спеціалізованих провайдерів.
Розглянемо модуль для роботи з простою базою даних у пам’яті.
Спочатку оголосимо типи конфігурації, токени та базові класи:
import {
DynamicModule,
Injectable,
Module,
Provider,
} from '@nestjs/common';
export interface DatabaseOptions {
databaseName: string;
}
export const DATABASE_OPTIONS = Symbol('DATABASE_OPTIONS');
export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION');
export type EntityClass<T> = new (...args: any[]) => T;
export function getRepositoryToken(
entity: EntityClass<unknown>,
): string {
return `Repository<${entity.name}>`;
}
export class InMemoryConnection {
private readonly collections = new Map<string, unknown[]>();
constructor(public readonly databaseName: string) {}
getCollection(name: string): unknown[] {
let collection = this.collections.get(name);
if (!collection) {
collection = [];
this.collections.set(name, collection);
}
return collection;
}
}
export class Repository<T extends object> {
constructor(
private readonly connection: InMemoryConnection,
private readonly entity: EntityClass<T>,
) {}
save(value: T): T {
const collection = this.connection.getCollection(this.entity.name);
collection.push(value);
return value;
}
findAll(): T[] {
const collection = this.connection.getCollection(this.entity.name);
return collection as T[];
}
}Тепер створимо DatabaseModule зі статичними методами forRoot() і forFeature():
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
const optionsProvider: Provider = {
provide: DATABASE_OPTIONS,
useValue: options,
};
const connectionProvider: Provider = {
provide: DATABASE_CONNECTION,
inject: [DATABASE_OPTIONS],
useFactory: (databaseOptions: DatabaseOptions) => {
return new InMemoryConnection(databaseOptions.databaseName);
},
};
return {
module: DatabaseModule,
global: true,
providers: [optionsProvider, connectionProvider],
exports: [DATABASE_OPTIONS, DATABASE_CONNECTION],
};
}
static forFeature(
entities: EntityClass<unknown>[],
): DynamicModule {
const repositoryProviders: Provider[] = entities.map((entity) => ({
provide: getRepositoryToken(entity),
inject: [DATABASE_CONNECTION],
useFactory: (connection: InMemoryConnection) => {
return new Repository(connection, entity);
},
}));
return {
module: DatabaseModule,
providers: repositoryProviders,
exports: repositoryProviders.map(
(provider) => provider.provide,
),
};
}
}forRootМетод forRoot() отримує конфігурацію та повертає динамічний модуль:
static forRoot(options: DatabaseOptions): DynamicModule {
// ...
}Провайдер DATABASE_OPTIONS зберігає передані параметри:
const optionsProvider: Provider = {
provide: DATABASE_OPTIONS,
useValue: options,
};Провайдер підключення залежить від DATABASE_OPTIONS:
const connectionProvider: Provider = {
provide: DATABASE_CONNECTION,
inject: [DATABASE_OPTIONS],
useFactory: (databaseOptions: DatabaseOptions) => {
return new InMemoryConnection(databaseOptions.databaseName);
},
};NestJS спочатку знайде DATABASE_OPTIONS, а потім передасть його у фабрику підключення.
У динамічному модулі обов’язково потрібно вказати поле module:
return {
module: DatabaseModule,
// ...
};Це посилання на клас, для якого створюється динамічна конфігурація.
global: trueУ прикладі forRoot() повертає:
global: trueЦе робить динамічний модуль глобальним. Експортовані провайдери DATABASE_OPTIONS і DATABASE_CONNECTION стають доступними в інших модулях без повторного імпорту DatabaseModule.forRoot().
Глобальну конфігурацію слід застосовувати для ресурсів, які справді є спільними для всього застосунку. Наприклад:
одне підключення до бази даних;
один клієнт черги;
один конфігураційний сервіс.
Не варто робити глобальними всі модулі без потреби, оскільки це ускладнює розуміння залежностей.
forFeatureforFeature() приймає список сутностей:
DatabaseModule.forFeature([User])Для кожної сутності створюється окремий провайдер репозиторію:
const repositoryProviders: Provider[] = entities.map((entity) => ({
provide: getRepositoryToken(entity),
inject: [DATABASE_CONNECTION],
useFactory: (connection: InMemoryConnection) => {
return new Repository(connection, entity);
},
}));Для User токен буде таким:
Repository<User>Для Order:
Repository<Order>Тому різні репозиторії не конфліктують між собою.
Репозиторій отримує спільне підключення через inject:
inject: [DATABASE_CONNECTION]Це означає, що forFeature() не створює нове підключення. Він використовує вже зареєстроване підключення з forRoot().
Провайдери репозиторіїв експортуються, щоб їх могли інжектити модулі, які імпортують DatabaseModule.forFeature():
exports: repositoryProviders.map(
(provider) => provider.provide,
),Важливо експортувати саме токени провайдерів. Якщо провайдер створений динамічно, NestJS має отримати доступ до того самого токена, за яким він був зареєстрований.
Створимо сутність User:
export class User {
constructor(
public readonly id: number,
public readonly name: string,
) {}
}Потім використаємо репозиторій у сервісі:
import {
Inject,
Injectable,
} from '@nestjs/common';
@Injectable()
export class UsersService {
constructor(
@Inject(getRepositoryToken(User))
private readonly usersRepository: Repository<User>,
) {}
create(user: User): User {
return this.usersRepository.save(user);
}
findAll(): User[] {
return this.usersRepository.findAll();
}
}Функціональний модуль імпортує лише потрібну функціональність:
import { Module } from '@nestjs/common';
@Module({
imports: [
DatabaseModule.forFeature([User]),
],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}Тепер UsersService має доступ до Repository<User>, але не залежить від способу створення підключення.
Нижче наведено мінімальний застосунок, який можна запустити в NestJS-проєкті:
import {
Controller,
DynamicModule,
Get,
Injectable,
Inject,
Module,
Post,
Body,
Provider,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
export interface DatabaseOptions {
databaseName: string;
}
export const DATABASE_OPTIONS = Symbol('DATABASE_OPTIONS');
export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION');
export type EntityClass<T> = new (...args: any[]) => T;
export function getRepositoryToken(
entity: EntityClass<unknown>,
): string {
return `Repository<${entity.name}>`;
}
export class InMemoryConnection {
private readonly collections = new Map<string, unknown[]>();
constructor(public readonly databaseName: string) {}
getCollection(name: string): unknown[] {
let collection = this.collections.get(name);
if (!collection) {
collection = [];
this.collections.set(name, collection);
}
return collection;
}
}
export class Repository<T extends object> {
constructor(
private readonly connection: InMemoryConnection,
private readonly entity: EntityClass<T>,
) {}
save(value: T): T {
const collection = this.connection.getCollection(this.entity.name);
collection.push(value);
return value;
}
findAll(): T[] {
const collection = this.connection.getCollection(this.entity.name);
return collection as T[];
}
}
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
const optionsProvider: Provider = {
provide: DATABASE_OPTIONS,
useValue: options,
};
const connectionProvider: Provider = {
provide: DATABASE_CONNECTION,
inject: [DATABASE_OPTIONS],
useFactory: (databaseOptions: DatabaseOptions) => {
return new InMemoryConnection(databaseOptions.databaseName);
},
};
return {
module: DatabaseModule,
global: true,
providers: [optionsProvider, connectionProvider],
exports: [DATABASE_OPTIONS, DATABASE_CONNECTION],
};
}
static forFeature(
entities: EntityClass<unknown>[],
): DynamicModule {
const repositoryProviders: Provider[] = entities.map((entity) => ({
provide: getRepositoryToken(entity),
inject: [DATABASE_CONNECTION],
useFactory: (connection: InMemoryConnection) => {
return new Repository(connection, entity);
},
}));
return {
module: DatabaseModule,
providers: repositoryProviders,
exports: repositoryProviders.map(
(provider) => provider.provide,
),
};
}
}
export class User {
constructor(
public readonly id: number,
public readonly name: string,
) {}
}
@Injectable()
export class UsersService {
constructor(
@Inject(getRepositoryToken(User))
private readonly usersRepository: Repository<User>,
) {}
create(user: User): User {
return this.usersRepository.save(user);
}
findAll(): User[] {
return this.usersRepository.findAll();
}
}
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService,
) {}
@Get()
findAll(): User[] {
return this.usersService.findAll();
}
@Post()
create(@Body() body: { id: number; name: string }): User {
return this.usersService.create(
new User(body.id, body.name),
);
}
}
@Module({
imports: [
DatabaseModule.forFeature([User]),
],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
@Module({
imports: [
DatabaseModule.forRoot({
databaseName: 'application',
}),
UsersModule,
],
})
export class AppModule {}
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
void bootstrap();Після запуску застосунку:
DatabaseModule.forRoot() створює спільне підключення.
UsersModule викликає DatabaseModule.forFeature([User]).
forFeature() створює Repository<User>.
UsersService отримує репозиторій за токеном.
Контролер використовує сервіс для обробки запитів.
Приклад створення користувача:
POST /users
Content-Type: application/json
{
"id": 1,
"name": "Olena"
}Отримання всіх користувачів:
GET /usersМетод forRoot() або forFeature() може повертати:
imports;
providers;
controllers;
exports;
global.
Наприклад:
static forRoot(): DynamicModule {
return {
module: SomeModule,
imports: [ConfigModule],
providers: [SomeProvider],
exports: [SomeProvider],
};
}useValueЦе зручно для статичних параметрів:
{
provide: DATABASE_OPTIONS,
useValue: options,
}Якщо налаштування залежать від інших провайдерів, можна використовувати асинхронний або синхронний useFactory. Наприклад, фабрика може отримати конфігураційний сервіс через inject:
const optionsProvider: Provider = {
provide: DATABASE_OPTIONS,
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
databaseName: configService.getOrThrow<string>('DATABASE_NAME'),
}),
};У такому випадку сам динамічний модуль повинен імпортувати модуль, який надає ConfigService, або отримати його через конфігурацію модуля.
Для конфігурації та спільних ресурсів зручно використовувати Symbol:
export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION');Для групи однотипних провайдерів токен можна будувати на основі сутності:
getRepositoryToken(User)Той самий вираз потрібно використовувати під час реєстрації та ін’єкції провайдера.
forRoot() кілька разів без потребиЯкщо forRoot() створює підключення, повторний виклик може створити кілька незалежних екземплярів.
Найчастіше правильна структура така:
@Module({
imports: [
DatabaseModule.forRoot(options),
UsersModule,
OrdersModule,
],
})
export class AppModule {}А функціональні модулі використовують лише forFeature():
@Module({
imports: [
DatabaseModule.forFeature([User]),
],
})
export class UsersModule {}exportsЯкщо провайдер створений у динамічному модулі, але не експортований, інші модулі не зможуть його інжектити.
Потрібно експортувати токен:
return {
module: DatabaseModule,
providers: [repositoryProvider],
exports: [getRepositoryToken(User)],
};Це не спрацює, якщо провайдер зареєстрований за одним токеном, а інжектиться за іншим:
// Реєстрація
provide: 'USER_REPOSITORY'
// Ін'єкція за іншим токеном
@Inject('Repository<User>')Для уникнення помилок використовуйте спільну функцію:
provide: getRepositoryToken(User)і:
@Inject(getRepositoryToken(User))forFeatureforFeature() не повинен створювати нове загальне підключення для кожної сутності. Його завдання — створити спеціалізовані провайдери поверх ресурсу, зареєстрованого через forRoot().
Правильна залежність має виглядати так:
forRoot() → DATABASE_CONNECTION
forFeature() → Repository<User> → DATABASE_CONNECTION@Module()Коли всі метадані формуються динамічно, базовий модуль може мати порожній декоратор:
@Module({})
export class DatabaseModule {}Це нормально: фактичні провайдери додаються об’єктом DynamicModule, який повертає статичний метод.
Динамічний модуль — це модуль, метадані якого формуються під час виклику статичного методу.
forRoot() зазвичай налаштовує спільний ресурс один раз.
forFeature() реєструє функціональні провайдери для конкретного модуля.
Кожен динамічний модуль повинен повертати поле module.
Провайдери, доступні за межами модуля, потрібно додавати до exports.
Для конфігурації та ресурсів зручно використовувати стабільні токени.
forFeature() може інжектити провайдери, створені через forRoot().
global: true слід використовувати лише для ресурсів, які справді мають бути доступні в усьому застосунку.