← Назад к вопросам

Что такое useFactory?

2.0 Middle🔥 171 комментариев
#JavaScript Core#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

useFactory: провайдер зависимостей в NestJS

useFactory — это один из способов создания провайдеров в NestJS. Это функция, которая возвращает значение или экземпляр, используемый для инъекции зависимостей (Dependency Injection). Это не React hook, а паттерн NestJS для динамического создания сервисов.

Контекст: Провайдеры в NestJS

В NestJS провайдер — это класс или функция, которые можно инъецировать в другие классы. Есть несколько способов определить провайдер:

// 1. Простой провайдер (class)
@Injectable()
class DatabaseService {
  constructor() {
    this.connection = null;
  }
}

// 2. useValue — статическое значение
const MY_CONFIG = {
  dbUrl: 'postgres://...',
  port: 5432
};

// 3. useFactory — динамическое создание
// 4. useClass — использование класса как провайдера

Что такое useFactory

useFactory позволяет динамически создавать провайдер на основе логики, конфигурации или других провайдеров:

// config.service.ts
@Injectable()
export class ConfigService {
  getDatabaseUrl() {
    return process.env.DATABASE_URL || 'postgres://localhost';
  }
}

// database.service.ts
@Injectable()
export class DatabaseService {
  constructor(private configService: ConfigService) {}
  
  connect() {
    const url = this.configService.getDatabaseUrl();
    console.log('Connecting to:', url);
  }
}

// app.module.ts
@Module({
  providers: [
    ConfigService,
    {
      provide: 'DATABASE_CONNECTION', // Токен провайдера
      useFactory: (configService: ConfigService) => {
        // Factory функция
        const url = configService.getDatabaseUrl();
        // Создаём и возвращаем экземпляр
        return {
          connect: () => console.log('Connecting to', url),
          url: url
        };
      },
      inject: [ConfigService] // Инъецируемые зависимости
    },
    DatabaseService
  ]
})
export class AppModule {}

Основные компоненты useFactory

{
  provide: 'TOKEN_NAME',           // 1. Токен, по которому будет инъецироваться
  useFactory: (dep1, dep2) => {   // 2. Factory функция, получает зависимости
    // Здесь может быть сложная логика
    return {
      method1() { },
      method2() { }
    };
  },
  inject: [Dependency1, Dependency2] // 3. Зависимости для factory функции
}

Примеры использования useFactory

1. Подключение к БД на основе конфигурации

// database.provider.ts
import { Knex, knex } from 'knex';

export const databaseProvider = {
  provide: 'DATABASE',
  useFactory: async (configService: ConfigService): Promise<Knex> => {
    const dbConfig = {
      client: 'postgresql',
      connection: configService.getDatabaseConfig(),
      migrations: { directory: './migrations' }
    };
    
    const db = knex(dbConfig);
    
    // Проверяем соединение
    try {
      await db.raw('SELECT 1');
      console.log('Database connected');
    } catch (error) {
      console.error('Database connection failed', error);
      throw error;
    }
    
    return db;
  },
  inject: [ConfigService]
};

// app.module.ts
@Module({
  providers: [ConfigService, databaseProvider]
})
export class AppModule {}

// users.service.ts
@Injectable()
export class UsersService {
  constructor(@Inject('DATABASE') private db: Knex) {}
  
  findAll() {
    return this.db('users').select();
  }
}

2. Redis кэш с асинхронной инициализацией

// cache.provider.ts
import Redis from 'ioredis';

export const cacheProvider = {
  provide: 'CACHE',
  useFactory: async (configService: ConfigService): Promise<Redis> => {
    const redis = new Redis({
      host: configService.get('REDIS_HOST'),
      port: configService.get('REDIS_PORT'),
      password: configService.get('REDIS_PASSWORD')
    });
    
    // Проверяем соединение
    await redis.ping();
    console.log('Redis connected');
    
    return redis;
  },
  inject: [ConfigService]
};

// posts.service.ts
@Injectable()
export class PostsService {
  constructor(@Inject('CACHE') private redis: Redis) {}
  
  async getPost(id: number) {
    // Пытаемся получить из кэша
    const cached = await this.redis.get(\`post:\${id}\`);
    if (cached) return JSON.parse(cached);
    
    // Если нет, получаем из БД
    const post = await this.db.query('SELECT * FROM posts WHERE id = ?', [id]);
    
    // Кэшируем на 1 час
    await this.redis.setex(\`post:\${id}\`, 3600, JSON.stringify(post));
    
    return post;
  }
}

3. JWT стратегия с динамической конфигурацией

// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    // Конфигурация получается динамически
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET')
    });
  }
  
  validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

// auth.module.ts
@Module({
  imports: [PassportModule, JwtModule.register({
    secret: configService.get('JWT_SECRET'),
    signOptions: { expiresIn: '1h' }
  })],
  providers: [AuthService, JwtStrategy]
})
export class AuthModule {}

4. Логгер с разными уровнями на основе окружения

// logger.provider.ts
import * as winston from 'winston';

export const loggerProvider = {
  provide: 'LOGGER',
  useFactory: (configService: ConfigService): winston.Logger => {
    const level = configService.get('LOG_LEVEL') || 'info';
    
    return winston.createLogger({
      level,
      format: winston.format.json(),
      transports: [
        new winston.transports.Console(),
        new winston.transports.File({ 
          filename: 'error.log', 
          level: 'error' 
        }),
        new winston.transports.File({ 
          filename: 'combined.log' 
        })
      ]
    });
  },
  inject: [ConfigService]
};

// app.module.ts
@Module({
  providers: [ConfigService, loggerProvider]
})
export class AppModule {}

// any.service.ts
@Injectable()
export class AnyService {
  constructor(@Inject('LOGGER') private logger: winston.Logger) {}
  
  doSomething() {
    this.logger.info('Doing something');
  }
}

useFactory vs useClass vs useValue

// 1. useValue — простое значение
{
  provide: 'APP_CONFIG',
  useValue: { port: 3000, host: 'localhost' }
}

// 2. useClass — использование класса
@Injectable()
export class DefaultLogger {}

{
  provide: 'LOGGER',
  useClass: DefaultLogger
}

// 3. useFactory — сложная логика создания
{
  provide: 'LOGGER',
  useFactory: (configService: ConfigService) => {
    if (configService.get('ENV') === 'production') {
      return new ProductionLogger();
    }
    return new DevelopmentLogger();
  },
  inject: [ConfigService]
}

// 4. useExisting — алиас для существующего провайдера
{
  provide: 'DEFAULT_LOGGER',
  useExisting: 'LOGGER'
}

Асинхронные useFactory провайдеры

useFactory поддерживает асинхронность — очень полезно для инициализации при запуске приложения:

// database.provider.ts
export const databaseProvider = {
  provide: 'DATABASE',
  useFactory: async (configService: ConfigService): Promise<any> => {
    const db = new Database(configService.getDatabaseConfig());
    await db.initialize();
    
    // Подключаем hooks для graceful shutdown
    process.on('SIGTERM', async () => {
      await db.close();
    });
    
    return db;
  },
  inject: [ConfigService]
};

// app.module.ts
@Module({
  imports: [
    // Нужно использовать асинхронный импорт модуля
    TypeOrmModule.forRootAsync({
      useFactory: async (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        database: configService.get('DB_NAME'),
        username: configService.get('DB_USER'),
        password: configService.get('DB_PASSWORD'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: false
      }),
      inject: [ConfigService]
    })
  ]
})
export class AppModule {}

Типичные ошибки

// ❌ Забыли inject
{
  provide: 'SERVICE',
  useFactory: (configService: ConfigService) => {
    // configService будет undefined!
  }
  // inject отсутствует
}

// ✅ Правильно
{
  provide: 'SERVICE',
  useFactory: (configService: ConfigService) => {
    return configService.getService();
  },
  inject: [ConfigService]
}

// ❌ useFactory должна возвращать значение
{
  provide: 'SERVICE',
  useFactory: (configService: ConfigService) => {
    // Функция без return — вернёт undefined
    configService.getService();
  },
  inject: [ConfigService]
}

// ✅ Правильно: явный return
{
  provide: 'SERVICE',
  useFactory: (configService: ConfigService) => {
    return configService.getService();
  },
  inject: [ConfigService]
}

Когда использовать useFactory

Используйте useFactory когда:

  1. Нужна сложная логика создания провайдера
  2. Провайдер зависит от конфигурации или других сервисов
  3. Нужна условная инициализация (разные экземпляры для разных окружений)
  4. Нужна асинхронная инициализация (подключение к БД, загрузка конфига)
  5. Нужны побочные эффекты при создании (логирование, очистка ресурсов)

Используйте useValue когда:

  • Провайдер это просто статическое значение

Используйте useClass когда:

  • Провайдер это класс без сложной логики инициализации

Итоговый вывод

useFactory в NestJS — это паттерн для динамического создания провайдеров с доступом к другим сервисам и конфигурации. Это мощный инструмент для:

  • Инициализации сложных сервисов
  • Условного создания разных экземпляров
  • Асинхронной инициализации ресурсов
  • Централизации логики создания объектов

Это не React hook, а бэкенд паттерн NestJS для управления зависимостями.