Building a Reusable ConfigService with NestJS Custom Providers
Create a flexible, testable configuration service in NestJS using a custom provider with useFactory. Learn how to register, inject, and verify it, plus trade‑offs and best practices.
01 Mar 2026, 01:35 UTC

Why You Need a Custom Provider for Configuration
In a typical NestJS app, configuration values come from environment variables or static files. When the configuration logic grows—fetching from a remote service, applying defaults, or merging multiple sources—you need a flexible, testable way to expose those values. A custom provider lets you encapsulate that logic in a single, injectable unit that can be swapped or mocked easily.
Core Concepts of NestJS Providers
- useClass: the framework creates an instance of the supplied class.
- useValue: a plain value is registered (good for static config objects).
- useFactory: a factory function runs at bootstrap to produce the value.
Tokens identify providers. They can be the class itself, a string, or a symbol. Using a unique symbol prevents accidental collisions, especially when multiple modules expose similar services.
Creating a ConfigProvider with useFactory
The factory pattern is ideal when you need to read environment variables, perform async work, or combine multiple sources. Below is a minimal but realistic example.
1. Define a token
// config.constants.ts
export const CONFIG_TOKEN = Symbol('CONFIG');
2. Build the factory
// config.provider.ts
import { ConfigService } from '@nestjs/config';
import { CONFIG_TOKEN } from './config.constants';
export const ConfigProvider = {
provide: CONFIG_TOKEN,
useFactory: async (configService: ConfigService) => {
// Load a remote config file or compute defaults
const remote = await fetchRemoteConfig(); // placeholder async call
const defaults = {
port: 3000,
logLevel: 'info',
};
return {
...defaults,
...remote,
// Override with environment variables if present
port: Number(process.env.PORT) || defaults.port,
};
},
inject: [ConfigService],
};
async function fetchRemoteConfig(): Promise<Record<string, any>> {
// Simulated async fetch
return { logLevel: 'debug' };
}
3. Register the provider in a module
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConfigProvider, CONFIG_TOKEN } from './config.provider';
import { DemoController } from './demo.controller';
@Module({
imports: [ConfigModule.forRoot()],
controllers: [DemoController],
providers: [
ConfigProvider,
{ provide: 'APP_CONFIG', useExisting: CONFIG_TOKEN }, // optional alias
],
})
export class AppModule {}
Consuming the Config in a Controller
// demo.controller.ts
import { Controller, Get } from '@nestjs/common';
import { Inject } from '@nestjs/common/decorators/core/inject.decorator';
import { CONFIG_TOKEN } from './config.constants';
@Controller()
export class DemoController {
constructor(
@Inject(CONFIG_TOKEN) private readonly config: Record<string, any>
) {}
@Get('/config')
getConfig() {
console.log('Current config:', this.config);
return this.config;
}
}
Verifying the Provider Works
- Create a new NestJS project:
nest new demo. - Add the
@nestjs/configpackage:npm i @nestjs/config. - Insert the files above into
src/.- Ensure
main.tsbootstrapsAppModule(default).
- Ensure
- Run the server:
npm run start. - Open
http://localhost:3000/configor inspect the console; you should see a logged object containing the merged configuration.
If the controller throws a Cannot resolve dependencies error, check that the token matches exactly and that ConfigModule.forRoot() is imported before the provider.
Trade‑offs and Limitations
- Complexity vs. Simplicity: For static values,
useValueis simpler. The factory pattern adds async support but increases code churn. - Scope Mismatch: By default providers are singletons. If you need request‑scoped configuration, you must declare
scope: Scope.REQUESTand be mindful of shared state. - Circular Dependencies: If
ConfigProviderdepends on another provider that in turn requires the config, NestJS throws a runtime error. UseforwardRef()or refactor the dependency chain. - Testing Overhead
When unit‑testing controllers that depend on the config, you’ll need to provide a mock token or use useValue for the test module.
Actionable Next Steps
- Wrap the provider in a dedicated
ConfigModuleso that other modules can import it without duplication. - Expose typed configuration interfaces to leverage TypeScript’s type safety.
- Consider caching the remote config in the factory to avoid repeated network calls on each bootstrap.
- Use
forwardRef()sparingly—prefer refactoring to eliminate circular dependencies.
Custom providers give you the full power of NestJS’s DI system while keeping configuration logic isolated, testable, and reusable across your application.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.