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
nodeEnv
variable is of typeany
. It should be of typeEnvironment
. - 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
-
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. -
What about the
process.env
? We can make it typesafe also by declaring atypes.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.