Custom Providers

The Long Form of a Provider

providers: [ProductsService] is short for { provide: ProductsService, useClass: ProductsService }. The long form lets you control what gets injected.

FormInjectsTypical use
useClassA new instance of a classSwap implementation (mock, different gateway)
useValueA fixed value or objectConstants, config objects, test mocks
useFactoryWhatever a function returns (can be async)Build a client from config
useExistingAn alias for another providerTwo tokens, one instance

All Four in One Module

src/payments/payments.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PaymentGateway, RazorpayGateway, FakeGateway } from './gateways';

export const GST_RATE = 'GST_RATE';
export const RAZORPAY_CLIENT = 'RAZORPAY_CLIENT';

@Module({
  imports: [ConfigModule],
  providers: [
    { provide: GST_RATE, useValue: 0.18 },
    {
      provide: PaymentGateway,
      useClass: process.env.NODE_ENV === 'test' ? FakeGateway : RazorpayGateway,
    },
    {
      provide: RAZORPAY_CLIENT,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        keyId: config.getOrThrow<string>('RAZORPAY_KEY_ID'),
        keySecret: config.getOrThrow<string>('RAZORPAY_KEY_SECRET'),
      }),
    },
    { provide: 'GATEWAY', useExisting: PaymentGateway },
  ],
  exports: [PaymentGateway],
})
export class PaymentsModule {}

Simulating the Four Forms

Example

A small registry that resolves the four provider forms the way NestJS does:

type Token = string;
type Provider =
  | { provide: Token; useValue: unknown }
  | { provide: Token; useClass: new () => unknown }
  | { provide: Token; useFactory: (...deps: any[]) => unknown; inject?: Token[] }
  | { provide: Token; useExisting: Token };

class Injector {
  private cache = new Map<Token, unknown>();
  constructor(private providers: Provider[]) {}
  get(token: Token): unknown {
    if (this.cache.has(token)) return this.cache.get(token);
    const p = this.providers.find((x) => x.provide === token);
    if (!p) throw new Error('No provider for ' + token);
    let value: unknown;
    if ('useValue' in p) value = p.useValue;
    else if ('useClass' in p) value = new p.useClass();
    else if ('useFactory' in p) value = p.useFactory(...(p.inject ?? []).map((t) => this.get(t)));
    else value = this.get(p.useExisting);
    this.cache.set(token, value);
    return value;
  }
}

class RazorpayGateway { name = 'razorpay'; }

const injector = new Injector([
  { provide: 'ENV', useValue: { RAZORPAY_KEY_ID: 'rzp_test_123' } },
  { provide: 'GATEWAY', useClass: RazorpayGateway },
  { provide: 'CLIENT', inject: ['ENV'], useFactory: (env: { RAZORPAY_KEY_ID: string }) => ({ key: env.RAZORPAY_KEY_ID }) },
  { provide: 'PAYMENTS', useExisting: 'GATEWAY' },
]);

console.log(injector.get('GATEWAY'));
console.log(injector.get('CLIENT'));
console.log('alias is same instance:', injector.get('PAYMENTS') === injector.get('GATEWAY'));

Output

RazorpayGateway { name: 'razorpay' }
{ key: 'rzp_test_123' }
alias is same instance: true