Skip to content
ExponaBLOG
NESTJS · DAY 005 / 100

Custom providers: useValue, useClass, useFactory in NestJS

The escape hatches for anything that is not a simple class.

Expona Admin8 min read

Introduction

By day five of learning NestJS, you've probably decorated a few classes with @Injectable() and watched the framework wire them together automatically. But what happens when you need to inject a third-party library, swap implementations between environments, or initialize something asynchronously before it's used? That's where custom providers: useValue, useClass, useFactory come in. They're the escape hatches for anything that is not a simple class—and once you master them, you'll handle 90% of the real-world injection scenarios that standard class providers can't solve.

The Problem: When Auto-Wiring Isn't Enough

NestJS's default provider system works beautifully for your own classes. You mark them @Injectable(), add them to a module's providers array, and inject them by type. But production codebases are messier. You'll need to inject a Redis client, a configuration object, a mock for testing, or an instance built from multiple environment variables. None of these fit the mold of a simple decorated class, so NestJS gives you three escape hatches: useValue, useClass, and useFactory.

useValue: Constants, Mocks, and Plain Objects

Use useValue when you want to inject a constant, a pre-built object, or a mock directly into your providers. This is the simplest custom provider and the one you'll reach for first.

Real-world scenario: You're building an API that uses Stripe, but your test suite shouldn't hit the real API. You create a mock Stripe client and inject it in your test module.

typescript
// stripe.mock.ts
export const mockStripeClient = {
  charges: {
    create: jest.fn().mockResolvedValue({ id: 'ch_test_123' }),
  },
};

// payment.module.ts
import { Module } from '@nestjs/common';
import { PaymentService } from './payment.service';
import { mockStripeClient } from './stripe.mock';

@Module({
  providers: [
    PaymentService,
    {
      provide: 'STRIPE_CLIENT',
      useValue: mockStripeClient,
    },
  ],
})
export class PaymentModule {}

In PaymentService, you inject it using @Inject() with the token 'STRIPE_CLIENT':

typescript
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class PaymentService {
  constructor(@Inject('STRIPE_CLIENT') private stripe: any) {}

  async charge(amount: number) {
    return this.stripe.charges.create({ amount });
  }
}

useValue is also perfect for configuration objects or API keys that don't need initialization:

typescript
@Module({
  providers: [
    {
      provide: 'API_KEY',
      useValue: process.env.API_KEY,
    },
    {
      provide: 'FEATURE_FLAGS',
      useValue: {
        darkMode: true,
        betaFeatures: false,
      },
    },
  ],
})
export class ConfigModule {}

useClass: Swapping Implementations per Environment

Use useClass when you want to provide different implementations of the same interface depending on your environment (dev, test, production) or other runtime conditions. This is your tool for the strategy pattern in dependency injection.

Real-world scenario: You have a file storage service. In development, you store files on disk. In production, you upload to AWS S3. Both implement the same StorageService interface.

typescript
// storage.service.ts (abstract or interface)
export interface IStorageService {
  upload(file: Buffer, key: string): Promise;
  download(key: string): Promise;
}

// local-storage.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class LocalStorageService implements IStorageService {
  async upload(file: Buffer, key: string) {
    // Write to disk
    return `/uploads/${key}`;
  }

  async download(key: string) {
    // Read from disk
    return Buffer.from('...');
  }
}

// s3-storage.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class S3StorageService implements IStorageService {
  async upload(file: Buffer, key: string) {
    // Upload to S3
    return `https://s3.amazonaws.com/bucket/${key}`;
  }

  async download(key: string) {
    // Download from S3
    return Buffer.from('...');
  }
}

In your module, use useClass to pick the right implementation:

typescript
import { Module } from '@nestjs/common';
import { LocalStorageService } from './local-storage.service';
import { S3StorageService } from './s3-storage.service';

const isProduction = process.env.NODE_ENV === 'production';

@Module({
  providers: [
    {
      provide: 'STORAGE_SERVICE',
      useClass: isProduction ? S3StorageService : LocalStorageService,
    },
  ],
})
export class StorageModule {}

Then inject it by token:

typescript
@Injectable()
export class FileUploadService {
  constructor(
    @Inject('STORAGE_SERVICE') private storage: IStorageService,
  ) {}

  async saveFile(file: Buffer, name: string) {
    const url = await this.storage.upload(file, name);
    return url;
  }
}

useFactory: Async Setup with Injected Config

Use useFactory when you need to run initialization logic, compose multiple dependencies, or set up something asynchronously (like connecting to a database or Redis) before it becomes available for injection. This is the most powerful and flexible option.

Real-world scenario: You're initializing a Redis client. It needs the connection URL from your config service, and you want to validate the connection before the app starts.

typescript
// redis.factory.ts
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';

export const redisFactory = async (configService: ConfigService) => {
  const url = configService.get('REDIS_URL');
  const redis = new Redis(url);

  // Validate connection before returning
  await redis.ping();
  console.log('Redis connected');

  return redis;
};

// redis.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { redisFactory } from './redis.factory';

@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: 'REDIS',
      useFactory: redisFactory,
      inject: [ConfigService],
    },
  ],
  exports: ['REDIS'],
})
export class RedisModule {}

The inject array tells NestJS which providers to pass as arguments to your factory function. It matches by token (string or class).

Inject it like this:

typescript
import { Injectable, Inject } from '@nestjs/common';
import { Redis } from 'ioredis';

@Injectable()
export class CacheService {
  constructor(@Inject('REDIS') private redis: Redis) {}

  async get(key: string) {
    return this.redis.get(key);
  }

  async set(key: string, value: string) {
    return this.redis.set(key, value);
  }
}

You can also use useFactory for simpler cases where you're just composing dependencies without async work:

typescript
@Module({
  providers: [
    DatabaseService,
    {
      provide: 'REPO_FACTORY',
      useFactory: (db: DatabaseService) => {
        return {
          users: db.getRepository('users'),
          posts: db.getRepository('posts'),
        };
      },
      inject: [DatabaseService],
    },
  ],
})
export class DataModule {}

Using @Inject with Non-Class Tokens

When your provider token is a string (like 'REDIS' or 'API_KEY') instead of a class, you must use the @Inject() decorator to tell NestJS which provider to inject. You can't rely on type-based injection.

Why? NestJS uses TypeScript's reflect-metadata to read type information at runtime. Strings and primitives don't have types to reflect on, so you have to be explicit.

typescript
// ❌ This won't work
constructor(private config: 'CONFIG') {}

// ✅ This works
constructor(@Inject('CONFIG') private config: any) {}

// ✅ Even better, use an interface for type safety
constructor(@Inject('CONFIG') private config: ConfigInterface) {}

A Complete Example: Database Connection

Here's how these three patterns work together in a realistic database module:

typescript
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createConnection } from 'typeorm';

@Module({
  providers: [
    // useValue: provide a constant
    {
      provide: 'DB_POOL_SIZE',
      useValue: 10,
    },

    // useFactory: async initialization with injected config
    {
      provide: 'DATABASE',
      useFactory: async (configService: ConfigService) => {
        const connection = await createConnection({
          type: 'postgres',
          host: configService.get('DB_HOST'),
          port: configService.get('DB_PORT'),
          username: configService.get('DB_USER'),
          password: configService.get('DB_PASSWORD'),
          database: configService.get('DB_NAME'),
        });
        return connection;
      },
      inject: [ConfigService],
    },

    // useClass: swap implementations per environment
    {
      provide: 'LOGGER',
      useClass: process.env.NODE_ENV === 'production'
        ? ProductionLogger
        : DevelopmentLogger,
    },
  ],
  exports: ['DATABASE', 'DB_POOL_SIZE', 'LOGGER'],
})
export class DatabaseModule {}

Common Mistakes

  • Forgetting to export. If your custom provider lives in one module but another module needs it, add it to the exports array. Custom providers don't auto-export like class providers sometimes do.
  • Not using @Inject for string tokens. String tokens require @Inject('TOKEN'). You can't inject by type. This is a runtime error waiting to happen if you forget.
  • Async factory without handling errors. If your factory throws during app startup (e.g., database connection fails), NestJS will crash. Test your factories in isolation and add proper error handling.
  • Circular dependencies in useFactory. If your factory injects a provider that depends on another provider that injects what your factory provides, you'll create a circular graph. NestJS will catch this, but it's easy to build accidentally.
  • Forgetting inject array. If your factory depends on other providers, always populate the inject array. Without it, NestJS has no way to know what to pass as arguments.

Key Takeaways

  • useValue is for constants, mocks, and pre-built objects. No dependencies, no async work.
  • useClass swaps class implementations at runtime. Perfect for environment-specific behavior.
  • useFactory handles complex initialization, async setup, and dependency composition. Pass dependencies via the inject array.
  • String tokens (and other non-class tokens) require @Inject(TOKEN) to inject. Type-based injection won't work.
  • Custom providers must be exported from their module if other modules need them.
  • Factories are evaluated at application bootstrap. If a factory fails, the app won't start. Test them carefully.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles