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
- Scaffold a new NestJS project using the Nest CLI.
- Create a schema using class-validator.
- 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:
- There's not .env file in our project but the app started successfully.
- The
nodeEnvvariable is of typeany. It should be of typeEnvironment. - The
configService.getfunction 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
-
Can we make the
ConfigServiceeasier 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. -
What about the
process.env? We can make it typesafe also by declaring atypes.tsfile 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.