Skip to content
TypeScript ts nestjs 3 min read

Custom Types & Providers

NestJS resolves most dependencies by class — you ask for UsersService and the container hands one over. But real applications also need to inject things that are not classes: an interface-typed abstraction, a configuration object, a third-party SDK instance, or a value computed at startup. For these you use custom providers and injection tokens, declared with useValue, useClass, useFactory, or useExisting. This page covers token design, asynchronous factory providers, and how to wrap Nest’s ConfigService so environment access is fully typed.

Why a token instead of a class

Class-based injection works because the class is a runtime value Nest can key on. Interfaces and primitives have no runtime identity, so you provide them against an explicit token — usually a Symbol or a string constant.

export interface PaymentGateway {
  charge(cents: number): Promise<{ id: string }>;
}

// A stable, collision-proof token
export const PAYMENT_GATEWAY = Symbol("PAYMENT_GATEWAY");

A Symbol token cannot collide with any other token in the app, which makes it safer than a bare string. The interface stays compile-time only; the token is the runtime handle the container uses.

The four provider recipes

Nest offers four ways to satisfy a token. Choosing the right one keeps wiring explicit and testable.

RecipeProvidesTypical use
useClassa class instanceswap implementations per environment
useValuea literal/objectconstants, mocks, external instances
useFactoryfactory return valuevalues needing computation or deps
useExistingan aliasexpose one provider under another token

useClass and useFactory create something; useValue and useExisting reuse something you already have. All four bind their result to a token that consumers reference with @Inject(token).

Binding an interface with useClass

To inject “any PaymentGateway”, register a concrete class against the interface’s token. Consumers depend on the abstraction, and you swap the implementation in one place.

import { Injectable, Inject, Module } from "@nestjs/common";

@Module({
  providers: [
    { provide: PAYMENT_GATEWAY, useClass: StripeGateway },
  ],
  exports: [PAYMENT_GATEWAY],
})
export class PaymentModule {}

@Injectable()
export class CheckoutService {
  constructor(
    @Inject(PAYMENT_GATEWAY) private readonly gateway: PaymentGateway,
  ) {}
}

CheckoutService never names StripeGateway; it only knows the PaymentGateway contract. In tests you re-provide PAYMENT_GATEWAY with useValue: fakeGateway and nothing else changes.

Factory providers and useFactory

A factory provider computes its value, optionally from other injected dependencies listed in inject. Factories may be async, which is ideal for connections that must be established before the app is ready.

{
  provide: "DATABASE_CONNECTION",
  useFactory: async (config: ConfigService) => {
    const url = config.get<string>("DATABASE_URL");
    return createConnection(url);
  },
  inject: [ConfigService],
}

The inject array tells Nest which providers to resolve and pass, in order, to the factory. Because the factory returns a Promise, Nest awaits it during bootstrap, so the connection is ready before any handler runs.

A strongly typed ConfigService

The default configService.get("KEY") returns string | undefined, which scatters non-null assertions through your code. Define a typed config shape and a thin typed wrapper so every read is checked and autocompleted.

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

interface AppConfig {
  DATABASE_URL: string;
  PORT: number;
  JWT_SECRET: string;
}

@Injectable()
export class TypedConfigService {
  constructor(private readonly config: ConfigService<AppConfig, true>) {}

  get<K extends keyof AppConfig>(key: K): AppConfig[K] {
    return this.config.get(key, { infer: true });
  }
}

The ConfigService<AppConfig, true> generic parameters declare the config shape and that validation guarantees presence (so reads are non-nullable). The get wrapper uses keyof AppConfig and an indexed access return type AppConfig[K], so config.get("PORT") is typed number and a typo like config.get("PROT") is a compile error.

Validate environment variables at startup (e.g. with a Joi or class-validator schema in ConfigModule.forRoot). Typing alone does not guarantee the value exists at runtime — it only describes what you expect.

Best Practices

  • Use Symbol injection tokens for interfaces and values to avoid string collisions.
  • Depend on interface tokens, not concrete classes, when you want swappable implementations.
  • Prefer useFactory (with inject) for values that need computation or other providers.
  • Use async factories for connections and SDKs that must initialize before bootstrap completes.
  • Export tokens from their module so other modules can inject them.
  • Wrap ConfigService with a typed facade using keyof/indexed access for safe env reads.
  • Validate environment variables at startup; static types describe intent, not runtime presence.
Last updated June 29, 2026
Was this helpful?