Урок 03

Модули

Что такое модуль

Модуль — контейнер для контроллеров, сервисов и других модулей. Группирует связанный код.

Каждое приложение NestJS — дерево модулей. Корень — AppModule.

Зачем модули

  • Изоляция — код фичи вместе.
  • Инкапсуляция — что отдавать наружу.
  • Переиспользование — модуль можно подключать.
  • Ленивая загрузка — в больших приложениях.
  • Тестирование — изолированно.

Создание

nest g module users

Что создаст:

import { Module } from '@nestjs/common';

@Module({})
export class UsersModule {}

И подключит в AppModule.

Метаданные @Module

@Module({
    imports: [],
    controllers: [],
    providers: [],
    exports: [],
})
export class UsersModule {}
  • imports — модули, чьи экспорты нужны.
  • controllers — HTTP-контроллеры.
  • providers — сервисы, фабрики.
  • exports — что доступно другим модулям.

Базовый модуль

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
    controllers: [UsersController],
    providers: [UsersService],
})
export class UsersModule {}

Всё внутри модуля — контроллер + сервис.

Импорт модуля

Модуль A хочет использовать сервис из модуля B:

users.module.ts:

@Module({
    controllers: [UsersController],
    providers: [UsersService],
    exports: [UsersService],
})
export class UsersModule {}

orders.module.ts:

import { UsersModule } from '../users/users.module';

@Module({
    imports: [UsersModule],
    controllers: [OrdersController],
    providers: [OrdersService],
})
export class OrdersModule {}

Теперь OrdersService может внедрить UsersService.

Экспорт

Без exports — провайдеры приватны внутри модуля.

@Module({
    providers: [UsersService],
    exports: [UsersService],   // доступен другим
})
export class UsersModule {}

Можно экспортировать:

  • Сервисы.
  • Другие модули.

Общие модули

Shared module — общие утилиты:

// logger.module.ts
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Global()
@Module({
    providers: [LoggerService],
    exports: [LoggerService],
})
export class LoggerModule {}

@Global() — не надо импортировать в каждом модуле. Только в корневом.

⚠️ Не злоупотребляй — теряется явность.

Feature-модули

Каждая фича — модуль:

src/
├── modules/
│   ├── users/
│   │   ├── users.controller.ts
│   │   ├── users.module.ts
│   │   └── users.service.ts
│   ├── posts/
│   └── auth/

Динамические модули

Настраиваемый модуль:

@Module({})
export class DatabaseModule {
    static forRoot(options: DatabaseOptions): DynamicModule {
        return {
            module: DatabaseModule,
            providers: [
                {
                    provide: 'DATABASE_OPTIONS',
                    useValue: options,
                },
                DatabaseService,
            ],
            exports: [DatabaseService],
        };
    }
}

Использование:

@Module({
    imports: [
        DatabaseModule.forRoot({
            host: 'localhost',
            port: 5432,
        }),
    ],
})
export class AppModule {}

Так делают TypeOrmModule.forRoot, ConfigModule.forRoot.

Асинхронные модули

Когда конфиг асинхронный:

DatabaseModule.forRootAsync({
    imports: [ConfigModule],
    useFactory: (config: ConfigService) => ({
        host: config.get('DB_HOST'),
        port: config.get<number>('DB_PORT'),
    }),
    inject: [ConfigService],
}),

Реэкспорт

Модуль может экспортировать импортированные модули:

@Module({
    imports: [CommonModule],
    exports: [CommonModule],
})
export class SharedModule {}

Транзитивно даёт доступ.

Порядок инициализации

NestJS сам разрешает зависимости между модулями. Порядок в imports не критичен.

Циклические зависимости — плохо. Разрывай через forwardRef() или события.

Циклические зависимости

⚠️ Модуль A ↔ Модуль B:

// a.module.ts
@Module({
    imports: [forwardRef(() => BModule)],
})
export class AModule {}

// b.module.ts
@Module({
    imports: [forwardRef(() => AModule)],
})
export class BModule {}

Или разорвать через EventEmitter.

Практика: Users + Posts

users/users.module.ts:

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
    controllers: [UsersController],
    providers: [UsersService],
    exports: [UsersService],
})
export class UsersModule {}

posts/posts.service.ts:

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class PostsService {
    constructor(private readonly usersService: UsersService) {}

    findAll() {
        return [
            { id: 1, title: 'Пост', author: this.usersService.findOne(1) },
        ];
    }
}

posts/posts.module.ts:

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { UsersModule } from '../users/users.module';

@Module({
    imports: [UsersModule],
    controllers: [PostsController],
    providers: [PostsService],
})
export class PostsModule {}

app.module.ts:

@Module({
    imports: [UsersModule, PostsModule],
})
export class AppModule {}

Кастомные провайдеры

Провайдер — не только класс:

useClass

{
    provide: LoggerService,
    useClass: process.env.NODE_ENV === 'production'
        ? FileLoggerService
        : ConsoleLoggerService,
}

useValue

{
    provide: 'API_KEY',
    useValue: process.env.API_KEY,
}

useFactory

{
    provide: 'DATABASE',
    useFactory: async (config: ConfigService) => {
        return await createConnection(config.get('DB_URL'));
    },
    inject: [ConfigService],
}

useExisting

{
    provide: 'AliasService',
    useExisting: RealService,
}

Инъекция кастомных провайдеров

constructor(
    @Inject('API_KEY') private apiKey: string,
    @Inject('DATABASE') private db: Database,
) {}

@Inject('токен') — для не-классов.

Практика: config-модуль

// config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()
@Module({
    providers: [ConfigService],
    exports: [ConfigService],
})
export class ConfigModule {}

config/config.service.ts:

import { Injectable } from '@nestjs/common';

@Injectable()
export class ConfigService {
    private config = {
        port: Number(process.env.PORT) || 3000,
        dbUrl: process.env.DATABASE_URL || '',
        jwtSecret: process.env.JWT_SECRET || '',
    };

    get<T>(key: keyof typeof this.config): T {
        return this.config[key] as T;
    }
}

Использование в любом сервисе:

constructor(private config: ConfigService) {
    const port = this.config.get<number>('port');
}

Проверка

nest g module users
# → создаст users/users.module.ts и подключит в AppModule

Итог

  • Модуль — контейнер для фичи.
  • imports, controllers, providers, exports — метаданные.
  • @Global() — общий модуль.
  • Dynamic modules — настройка через forRoot.
  • Циклические — forwardRef.
  • Кастомные провайдеры — useClass, useValue, useFactory.
  • @Inject — для токенов.

Дальше — контроллеры.