NestJSMiddleTechnical

В чём разница между forRoot() и forFeature() при регистрации модулей?

forRoot() конфигурирует глобальный singleton-ресурс (соединение с БД) один раз в AppModule; forFeature() регистрирует конкретные сущности/схемы внутри фича-модуля и предоставляет их репозитории только в нём.

forRoot() и forFeature() в NestJS

Это конвенция для динамических модулей, которая позволяет один раз сконфигурировать глобальный ресурс (forRoot) и многократно подключить его части в отдельных фича-модулях (forFeature). Классические примеры — TypeOrmModule, MongooseModule, SequelizeModule.

forRoot — глобальная конфигурация

forRoot() регистрирует соединение (или другой тяжёлый singleton) один раз в корневом модуле и помечает его global: true, чтобы сделать провайдеры доступными во всём приложении без повторного импорта.

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'secret',
      database: 'app',
      autoLoadEntities: true,
      synchronize: false,
    }),
  ],
})
export class AppModule {}

forFeature — регистрация сущностей в конкретном модуле

forFeature() принимает список сущностей (или схем) и предоставляет репозитории только внутри текущего модуля через InjectRepository:

// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
  exports: [UsersService],
})
export class UsersModule {}
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.userRepo.find();
  }
}

Собственный динамический модуль с forRoot/forFeature

Можно реализовать тот же паттерн самостоятельно:

// cache.module.ts
import { DynamicModule, Global, Module } from '@nestjs/common';

export interface CacheModuleOptions {
  ttl: number;
  host: string;
}

@Global()
@Module({})
export class CacheModule {
  static forRoot(options: CacheModuleOptions): DynamicModule {
    return {
      module: CacheModule,
      providers: [
        { provide: 'CACHE_OPTIONS', useValue: options },
        CacheService,
      ],
      exports: [CacheService],
    };
  }

  static forFeature(namespace: string): DynamicModule {
    return {
      module: CacheModule,
      providers: [
        {
          provide: `CACHE_NAMESPACE_${namespace.toUpperCase()}`,
          useFactory: (svc: CacheService) => svc.createNamespace(namespace),
          inject: [CacheService],
        },
      ],
      exports: [`CACHE_NAMESPACE_${namespace.toUpperCase()}`],
    };
  }
}

forRootAsync — асинхронная конфигурация

Когда настройки нужно получить из ConfigService, используется forRootAsync:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    type: 'postgres',
    url: config.get<string>('DATABASE_URL'),
    autoLoadEntities: true,
    synchronize: false,
  }),
})

Подводные камни

  • Вызывать forRoot() дважды с разными конфигурациями нельзя — второй вызов молча перезапишет первый. Для нескольких соединений используй именованные коннекции через name опцию.
  • Если модуль помечен @Global() и зарегистрирован через forRoot(), дополнительный импорт в других модулях не нужен. Повторный импорт не ломает, но создаёт путаницу.
  • forFeature() создаёт отдельный токен репозитория — InjectRepository(Entity) работает только в том модуле, который вызвал forFeature([Entity]). Использование репозитория в другом модуле без импорта даст ошибку DI.
  • Не забывай экспортировать TypeOrmModule из фича-модуля, если дочерний модуль тоже должен инжектировать тот же репозиторий.
  • При использовании synchronize: true в forRoot в продакшене — риск потери данных. Всегда выключай в production.
  • Асинхронная конфигурация через useFactory в forRootAsync выполняется при старте приложения. Если фабрика бросает исключение, приложение не запустится без внятной ошибки.
  • autoLoadEntities: true подгружает только сущности, зарегистрированные через forFeature. Сущности, не включённые ни в один модуль, не будут замечены TypeORM.

Common mistakes

  • Дает общий ответ про Node.js и не называет конкретные API NestJS.
  • Не объясняет, где в lifecycle находится forRoot() и forFeature().
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить forRoot() и forFeature() на примере кода.
  • Называет ключевые API: forRoot(), forFeature().
  • Использует точные API NestJS, а не вымышленные hooks/decorators/methods.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics