How to validate configuration per module in NestJs
I needed to configure an application with NestJs and I used the built in ConfigModule.
I wanted to have a configuration service that was specific to a given module to maintain the modularity of my application.
I also wanted to validate the configuration values. It wasn't clear from the nestjs documentation how to achieve a configuration service for a feature while also validating the properties. Here is how I did this.
Nest JS Configuration service
The ConfigService provided by NestJS is a dynamic module and has two static configuration methods. You can configure it forFeature()
or forRoot()
.
If you configure forRoot you load the entire set of environment variables in to the configuration service. Then you can inject this into any class.
// example: basic usage with forRoot()
// App module
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
// then in some service
constructor(private configService: ConfigService) {}
myMethod(){
const appPort = this.configService.get<string>('PORT');
}
This is an awesome way to read env vars in an injectable way that makes it easy to test and understand. A really nice feature here is that the forRoot()
method accepts validation to ensure all the environment variables are present as expected on application startup.
Validation with Nest JS configuration service
There are two built-in ways to validate the configuration in nestjs and both are properties provided to the forRoot()
method. The first property validationSchema
enables you to provide a joi validation. The second property is a validate
method you can pass in. This method can be fully customised and must return a boolean.
Configuration settings for features
For simple applications the forRoot()
method and a single global configuration service is good enough. For larger modular applications you probably want to split the configuration by feature rather than have a global configuration service. A global configuration service would quickly get large and difficult to work with across teams.
Nest configuration service supports this requirement with the forFeature()
configuration method. You can pass a schema object that describes the environment variables that should be loaded for a given feature.
// my feature configuration (./config/myFeature.config)
export default registerAs("myFeatureConfig", () => ({
setting1: process.env.VAR1,
setting2: process.env.VAR2,
}));
// my feature.module.ts
import myFeatureConfig from "./config/myFeature.config";
@Module({
imports: [ConfigModule.forFeature(myFeatureConfig)],
})
export class MyFeatureModule {}
This is great because it keeps the specific settings for the feature in one small configuration service.
Per feature Nestjs configuration with validation
The issue with forFeature()
is that you can't pass the same configuration properties in! So you can have a feature specific configuration or a validated configuration but not both at the same time.
To get around this I did some custom validation through a base class. The custom validation uses class-validator which is already in use with the nestjs framework. Here is how it works.
First create a base configuration service. Because we want to know if a configuration setting is available on startup we have this service use class validator to validate the properties on module initialisation.
import { Injectable, OnModuleInit } from "@nestjs/common";
import { validate } from "class-validator";
@Injectable()
export abstract class ValidatedConfigService implements OnModuleInit {
async onModuleInit(): Promise<void> {
const result = await validate(this);
if (result.length > 0) {
throw new Error(
`Configuration failed - Is there an environment variable missing?
${JSON.stringify(
result.map((v) => {
return {
property: v.property,
constraints: v.constraints,
};
}),
null,
2
)}`
);
}
}
}
Next you register the variables you will be using with nestjs's configuration service
import { registerAs } from "@nestjs/config";
export default registerAs("myFeature", () => ({
setting1: process.env.VAR1,
setting2: process.env.VAR2,
}));
To use this service in our feature module we extend a class with it. Then we decorate the class properties with our validation methods.
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class MyFeatureConfigService extends ValidatedConfigService {
constructor(private configService: ConfigService) {
super();
}
@IsBoolean()
get setting1(): boolean {
return this.configService.get<string>("myFeature.setting1") === "value1";
}
@IsString()
@IsDefined()
get setting2(): string {
return (
this.configService.get<string>("myFeature.setting2") || "defaultValue2"
);
}
}
And now we can inject our configuration service into any service as usual. It will be guaranteed to have been validated on application startup and it will only contain relevant settings for the specific feature.
@Injectable()
export default class MyFeatureService {
constructor(private readonly configService: MyFeatureConfigService) {
}
Conclusion
That's how validation and per feature configuration fits together in NestJS. Hope that's helpful!
If you use NestJs check out my eslint plugin for nestjs - https://www.npmjs.com/package/@darraghor/eslint-plugin-nestjs-typed