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.
| Recipe | Provides | Typical use |
|---|---|---|
useClass | a class instance | swap implementations per environment |
useValue | a literal/object | constants, mocks, external instances |
useFactory | factory return value | values needing computation or deps |
useExisting | an alias | expose 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-validatorschema inConfigModule.forRoot). Typing alone does not guarantee the value exists at runtime — it only describes what you expect.
Best Practices
- Use
Symbolinjection tokens for interfaces and values to avoid string collisions. - Depend on interface tokens, not concrete classes, when you want swappable implementations.
- Prefer
useFactory(withinject) 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
ConfigServicewith a typed facade usingkeyof/indexed access for safe env reads. - Validate environment variables at startup; static types describe intent, not runtime presence.