Антон Ларичев

Введение
Организация модулей в NestJS — это фундамент, на котором строится масштабируемость всего проекта. Когда модульная архитектура NestJS выстроена правильно, добавление новых функций не превращается в болезненную переработку половины кодовой базы. Но если структуру проекта NestJS запустить с самого начала, через полгода вы получите монолит, где все зависит от всего.
В этой статье разберем, как разделить модули в NestJS по доменным областям, какие типы модулей существуют и как избежать типичных ошибок при проектировании архитектуры.
Почему модульная архитектура NestJS так важна
NestJS изначально спроектирован вокруг концепции модулей. Декоратор @Module() — это основной строительный блок приложения. Он группирует контроллеры, сервисы и провайдеры в логически связанные единицы.
Проблема в том, что многие разработчики создают модули формально: один модуль на одну сущность базы данных. В итоге UserModule импортирует OrderModule, который импортирует PaymentModule, а тот снова зависит от UserModule. Появляются циклические зависимости, и проект теряет модульность.
// Плохо: модуль привязан к сущности, а не к домену
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
exports: [UserService], // экспортируем всё подряд
})
export class UserModule {}
Разделение модулей NestJS по доменным областям
Вместо создания модуля на каждую сущность, группируйте код по бизнес-доменам. Домен — это область бизнес-логики, которая может существовать относительно независимо.
Например, вместо отдельных UserModule, ProfileModule и AvatarModule создайте один AccountModule, объединяющий всё, что связано с учетной записью пользователя:
// Хорошо: модуль организован по домену
@Module({
imports: [TypeOrmModule.forFeature([User, Profile, Avatar])],
controllers: [AccountController, ProfileController],
providers: [
AccountService,
ProfileService,
AvatarService,
],
exports: [AccountService], // экспортируем только публичный API
})
export class AccountModule {}
Такой подход дает три преимущества. Во-первых, уменьшается количество модулей и связей между ними. Во-вторых, весь связанный код находится рядом. В-третьих, модуль можно легко вынести в отдельный микросервис.
Как правильно использовать exports и imports в модулях
Главное правило: экспортируйте из модуля минимум. Каждый экспортированный провайдер — это публичный контракт, который потом сложно изменить.
@Module({
imports: [AccountModule, ProductModule],
controllers: [OrderController],
providers: [
OrderService,
OrderRepository,
PriceCalculator, // внутренний провайдер, не экспортируем
],
exports: [OrderService], // только фасад для других модулей
})
export class OrderModule {}
Если другому модулю нужна функциональность, он должен работать через экспортированный сервис, а не напрямую обращаться к внутренним провайдерам. Это и есть инкапсуляция на уровне модулей.
Shared-модули для общей функциональности
Некоторые провайдеры нужны повсюду: логирование, конфигурация, работа с кешем. Для них создается shared-модуль:
@Module({
providers: [LoggerService, CacheService, ConfigService],
exports: [LoggerService, CacheService, ConfigService],
})
export class SharedModule {}
Если модуль используется почти везде, можно сделать его глобальным через декоратор @Global():
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
Но не злоупотребляйте @Global(). Глобальные модули нарушают явность зависимостей — непонятно, откуда берется провайдер. Используйте глобальные модули только для инфраструктурных вещей.
Динамические модули для гибкой конфигурации
Динамические модули позволяют настраивать поведение при импорте. Это удобно для библиотечных модулей, которые должны работать с разными параметрами:
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
global: true,
};
}
static forFeature(entities: Type[]): DynamicModule {
return {
module: DatabaseModule,
providers: entities.map((entity) => ({
provide: getRepositoryToken(entity),
useFactory: (db: DatabaseService) => db.getRepository(entity),
inject: [DatabaseService],
})),
exports: entities.map((entity) => getRepositoryToken(entity)),
};
}
}
Паттерн forRoot / forFeature позволяет разделить инициализацию (один раз в AppModule) и использование (в каждом фича-модуле).
Структура проекта NestJS для масштабируемости
Рекомендуемая структура директорий для среднего и крупного проекта:
src/
modules/
account/
account.module.ts
account.controller.ts
account.service.ts
dto/
entities/
order/
order.module.ts
order.controller.ts
order.service.ts
dto/
entities/
payment/
payment.module.ts
payment.controller.ts
payment.service.ts
shared/
shared.module.ts
logger/
cache/
config/
config.module.ts
app.module.ts
main.ts
Каждый доменный модуль содержит всё необходимое: контроллеры, сервисы, DTO и сущности. Это позволяет при необходимости вынести модуль целиком в отдельный микросервис.
Частые ошибки при организации модулей
Модуль на каждую сущность. Создание UserModule, UserProfileModule, UserSettingsModule по отдельности приводит к десяткам мелких модулей с перекрестными зависимостями. Объединяйте связанные сущности в один доменный модуль.
Циклические зависимости. Если ModuleA импортирует ModuleB, а ModuleB импортирует ModuleA, используйте forwardRef(). Но лучше пересмотреть архитектуру — циклические зависимости сигнализируют о неправильном разделении ответственности.
// Костыль, а не решение
@Module({
imports: [forwardRef(() => OrderModule)],
})
export class UserModule {}
Экспорт всех провайдеров. Когда модуль экспортирует все свои сервисы, репозитории и хелперы, он теряет инкапсуляцию. Экспортируйте только фасадный сервис.
God Module. Один модуль, который импортирует и экспортирует всё подряд. Обычно это AppModule, который разрастается до сотен строк. Держите AppModule чистым — он только импортирует доменные модули.
Заключение
Правильная организация модулей в NestJS — это не про количество файлов и папок, а про четкие границы между доменами. Разделяйте код по бизнес-областям, экспортируйте минимум, используйте shared-модули для инфраструктуры и динамические модули для гибкой конфигурации. Такая модульная архитектура NestJS позволит масштабировать проект без превращения его в монолит, а при необходимости — легко выделить любой модуль в отдельный сервис.






Комментарии
0