Factory Providers
Most of the time you let Nest construct a provider by simply listing its class, but sometimes the instance you need cannot be known until runtime. A factory provider lets you supply a function that computes the value to register against a token, optionally pulling in other providers to help build it. This is the escape hatch for configuration-driven instances, third-party clients that need bootstrapping, and any value that depends on data resolved at startup rather than at compile time.
What is a factory provider?
A factory provider is a custom provider defined with the useFactory property. Instead of handing Nest a class to instantiate, you give it a function. Nest calls that function once (for singletons) when the container builds the dependency graph and registers whatever it returns under the given token. The return value can be anything — a class instance, a configured object, a connection handle, or a primitive — because the token, not a constructor, is what consumers inject.
The three properties you work with are provide, useFactory, and the optional inject array:
| Property | Purpose |
|---|---|
provide | The injection token consumers will ask for (a class, string, or Symbol). |
useFactory | The function that returns the instance to register. |
inject | An ordered list of tokens whose resolved values are passed as arguments to the factory. |
A basic factory
In its simplest form a factory takes no dependencies and just computes a value. Here we build a configured options object that other services can inject by token.
// config/app-options.provider.ts
import { Provider } from '@nestjs/common';
export const APP_OPTIONS = 'APP_OPTIONS';
export const appOptionsProvider: Provider = {
provide: APP_OPTIONS,
useFactory: () => ({
retries: Number(process.env.HTTP_RETRIES ?? 3),
timeoutMs: Number(process.env.HTTP_TIMEOUT_MS ?? 5_000),
featureFlags: (process.env.FEATURES ?? '').split(',').filter(Boolean),
}),
};
Register it like any other provider and inject the value using @Inject with the token.
// app.module.ts
import { Module } from '@nestjs/common';
import { appOptionsProvider } from './config/app-options.provider';
import { HttpClientService } from './http-client.service';
@Module({
providers: [appOptionsProvider, HttpClientService],
})
export class AppModule {}
// http-client.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { APP_OPTIONS } from './config/app-options.provider';
interface AppOptions {
retries: number;
timeoutMs: number;
featureFlags: string[];
}
@Injectable()
export class HttpClientService {
constructor(@Inject(APP_OPTIONS) private readonly options: AppOptions) {}
describe(): string {
return `retries=${this.options.retries} timeout=${this.options.timeoutMs}ms`;
}
}
Output:
retries=3 timeout=5000ms
Injecting dependencies into the factory
The real power of factory providers shows up when the value you build depends on other providers. List those tokens in the inject array and Nest resolves each one and passes it to your factory in the same order. The factory parameters line up positionally with the inject entries.
// db/connection.provider.ts
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DataSource } from 'typeorm';
export const DATA_SOURCE = 'DATA_SOURCE';
export const dataSourceProvider: Provider = {
provide: DATA_SOURCE,
useFactory: (config: ConfigService) => {
return new DataSource({
type: 'postgres',
url: config.getOrThrow<string>('DATABASE_URL'),
entities: [__dirname + '/../**/*.entity.{ts,js}'],
synchronize: false,
});
},
inject: [ConfigService],
};
Because ConfigService is listed in inject, Nest guarantees it is fully constructed before the factory runs. The factory becomes a node in the dependency graph just like a class would, so ordering and lifetime are handled for you.
The order of arguments to
useFactorymust match the order of tokens ininjectexactly. They are positional, not name-based — swapping two entries silently passes the wrong instance to your factory.
Async factories
When building an instance requires awaiting something — opening a database connection, fetching remote configuration, reading a secret — return a Promise from the factory or mark it async. Nest awaits the resolved value before the container considers the provider ready, and before any consumer is constructed.
// db/connection.provider.ts
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DataSource } from 'typeorm';
export const DATA_SOURCE = 'DATA_SOURCE';
export const dataSourceProvider: Provider = {
provide: DATA_SOURCE,
useFactory: async (config: ConfigService): Promise<DataSource> => {
const dataSource = new DataSource({
type: 'postgres',
url: config.getOrThrow<string>('DATABASE_URL'),
entities: [__dirname + '/../**/*.entity.{ts,js}'],
});
await dataSource.initialize();
return dataSource;
},
inject: [ConfigService],
};
Now anything that injects DATA_SOURCE receives an already-connected DataSource. The application bootstrap pauses until every async factory in the graph has resolved, so a failed connection surfaces as a startup error rather than a runtime surprise on the first request.
// users/users.repository.ts
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { DATA_SOURCE } from '../db/connection.provider';
@Injectable()
export class UsersRepository {
constructor(@Inject(DATA_SOURCE) private readonly db: DataSource) {}
async count(): Promise<number> {
return this.db.getRepository('User').count();
}
}
Async factories are awaited during bootstrap, so a slow or hanging factory delays your entire application startup. Apply sensible connection timeouts and fail fast instead of retrying indefinitely inside the factory.
Best practices
- Use a
Symbolor exported string constant as the token and keep it next to the factory so consumers import one canonical value. - Keep factories focused on construction — build and configure the instance, but push business logic into a proper
@Injectable()service. - Always declare every runtime dependency in the
injectarray rather than reaching for globals likeprocess.envdeep inside a factory; it keeps the graph explicit and testable. - Prefer async factories over manual
OnModuleInitwiring when an instance is unusable until a connection or fetch completes. - Match
useFactoryparameters to theinjectarray order precisely, and type each parameter so the compiler catches mismatches early. - Reach for
registerAsyncpatterns in dynamic modules when a whole module’s configuration is factory-driven, rather than scattering loose factory providers.