Typesafe Config Service in NestJS

In this tutorial, we'll be learning how to make the NestJS config service typesafe. Our objective is to prevent our app from starting before validating the enviroment variables. I want to provide config service with a better developer experience and I want to utilize packages I already have which are class-validator and class-transformer.

Steps

  1. Scaffold a new NestJS project using the Nest CLI.
  2. Create a schema using class-validator.
  3. Infer types.

1. Scaffold a new NestJS project using the Nest CLI.

Here, We'll need an empty NestJS project. You can find the steps in the official documentation. We'll also need to install @nestjs/config, class-validator and class-transformer packages.

nest new typesafe-config
cd typesafe-config
npm i --save @nestjs/config class-validator class-transformer

We'll focus now on the src folder it should look something like that:

├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts

In app.module.ts we'll import the ConfigModule from @nestjs/config and register it in the imports array. Now we'are ready to use our environment variables. Now let's try to use the config service in our app.service.ts file.

import { ConfigService } from '@nestjs/config';
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  constructor(private readonly configService: ConfigService) {}
  getHello(): string {
    const nodeEnv = this.configService.get('NODE_ENV');
    //    ^?const nodeEnv: any
    return 'Hello World!';
  }
}

Currently we've 3 problems:

  1. There's not .env file in our project but the app started successfully.
  2. The nodeEnv variable is of type any. It should be of type Environment.
  3. The configService.get function does not suggest any keys.

2. Create a schema using class-validator.

We'll create a schema using class-validator. We'll create a new file .env.validation.ts and We'll borrow the snippet from NestJS documentation with minor modifications. We'll pass the validate function inside the ConfigModule.forRoot({validte}).

// .env.validation.ts
import { plainToInstance } from 'class-transformer';
import { IsEnum, IsNumber, validateSync } from 'class-validator';

enum Environment {
  Development = 'development',
  Production = 'production'
}

export class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToInstance(EnvironmentVariables, config, {
    enableImplicitConversion: true
  });
  const errors = validateSync(validatedConfig, {
    skipMissingProperties: false
  });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}
// app.module.ts
@Module({
  imports: [ConfigModule.forRoot({ validate })],
  controllers: [AppController],
  providers: [AppService],
})

Now if we try to start the app we'll get those errors.

Error: An instance of EnvironmentVariables has failed the validation:
 - property NODE_ENV has failed the following constraints: isEnum
,An instance of EnvironmentVariables has failed the validation:
 - property PORT has failed the following constraints: isNumber

Now we've validated our environment variables but we still don't have typesafety.

Now we need to create a .env file and add the following variables NODE_ENV=development and PORT=3000.

3. Infer types.

In order to infer the types of the environment variables we've to pass the EnvironmentVariables class to the ConfigService as a generic type. Also we'll need to pass the infer option to the get function in order to infer the type of the variable.

import { ConfigService } from '@nestjs/config';
import { EnvironmentVariables } from './env.validation';
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  constructor(
    private readonly configService: ConfigService<EnvironmentVariables>
  ) {}
  getHello(): string {
    const nodeEnv = this.configService.get('NODE_ENV', {
      //    ^? const nodeEnv: Environment
      infer: true
    });

    return 'Hello World!';
  }
}

Congratulations! Now the IDE will suggest the right enviroment variables names and you will get the right types.

Bonus

  1. Can we make the ConfigService easier to be used? Yes, but we'll need to create a custom config module and a custom config service that lift the heavy lifiting for us.

  2. What about the process.env? We can make it typesafe also by declaring a types.ts file in the root of our project and add the following code. For more info you can check this video by the legend Matt Pocock.

//types.ts
import { EnvironmentVariables } from './env.validation';
export {};
declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace NodeJS {
    interface ProcessEnv extends EnvironmentVariables {}
  }
}

Conclusion

In this tutorial, we've learned how to make the NestJS config service typesafe. We've also learned how to validate the environment variables using class-validator. You can find the source code here. Thanks for reading.