How to validate configuration per module in NestJs

Published on October 10, 2021

Tagged: #nestjs

Follow me on twitter for more posts like this

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

Darragh ORiordan

Hi! I'm Darragh ORiordan.

I live and work in Sydney, Australia building supporting happy teams that create high quality software for the web.

I also make tools for busy developers! Do you have a new M1 Mac to setup? Have you ever spent a week getting your dev environment just right?

My DevShell tooling will save you 30+ hours configuring your dev environment with all the best modern tools. Get it here

https://darraghoriordan.gumroad.com/l/devshell


Read more articles like this one...

List of article summaries

#nestjs

A Nest JS Pipeline Cheatsheet

I’m always checking the NestJS documentation to read what a Middleware Vs Interceptor is and I could never remember the order that the different NestJS pipeline elements are called in.

So I made this cheatsheet for myself so I had something to refer to.

#nestjs

Using a dynamic DTO property in a NestJS API

NestJS is designed around using strictly typed properties on models but sometimes it’s useful (and fast!) to allow dynamic types on properties and just store some business domain data as a dynamic serialized blob.

This is the serialized LOB method recommended by Martin Fowler (https://martinfowler.com/eaaCatalog/serializedLOB.html).

Here is how you can have a LOB in a NestJS REST Api with type safety and support for OpenAPI definitions.

#nestjs

Automatically setting empty arrays instead of undefined on typeorm entities

If you have an array on an entity model in typeorm you will have to handle the response when there are no items available to put in the array.

In my application I return the entity model directly for GET requests and I like having arrays as empty instead of undefined properties.

By default typeorm will not set the property to [] so it will be undefined.

Here is how to set an array property to have an empty array instead of undefined.

#nestjs

Fixing validation error in NestJS when using forbidUnknwonValues

If you get unexplained validation errors (http status 400) when calling a NestJS api you might want to check your Query() parameters. Parsing and validating these the wrong way can cause issues.