Config Module with environment variables
- Config Module with environment variables
Install dependencies
In a classic NodeJS project, you'd need to install the dotenv package to use environment variables.
NestJS comes with a built-in config module (that uses the dotenv package under the hood) that you can use to read environment variables.
npm i --save @nestjs/config
Add the Config Module configuration
With the package installed, we can now use the config module.
Import it into the root AppModule along with the forRoot() static method:
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
],
})
export class AppModule {}
Please note: As you add more imports to your app.module.ts file, keep the ConfigModule as the first import. Otherwise the other imports won't have access to the environment variables.
You can now use process.env
Assuming you're using the default .env file in your project, you'll now have access to your environment variables by using process.env anywhere in your NestJS app.
For example, you could use an environment variable to dynamically set the port of your app (this is required if you're deploying your NestJS app to Cloud Run) with a fallback value:
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ? parseInt(process.env.PORT) : 3000);
}
bootstrap();
While this approach works, it doesn't offer any type safety. It also means if you use a fallback (like in the example above), everytime you use the environment variable you'd need to define the fallback again. We'll go into a more advanced approach in the next section which will cover these challenges.
Using custom configuration files
Instead of using process.env in your NestJS app whenever you need to access an environment variable, you can instead use custom configuration files.
Laravel uses a very similar approach where you have custom configuration files inside a config directory which point to environment variables.
For example, here's a configuration file inside a config directory:
config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT) || 3000,
pokemonService: {
apiKey: process.env.POKEMEON_KEY,
}
});
You will need to import this configuration file into the ConfigModule by using the load property:
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import config from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [config]
}),
],
})
export class AppModule {}
To now use the values set in the configuration file in one of the modules in our NestJS app, we'd need to import the ConfigModule (just like you would with any provider):
feature.module.ts
@Module({
imports: [ConfigModule],
*// ...*})
That being said, I prefer to set the isGlobal property to true in the ConfigModule in app.module.ts so that it's available everywhere in the app (and I don't need to import the Config Module everytime).
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import config from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [config]
}),
],
})
export class AppModule {}
By the way, if you'd prefer to make your configuration files more granular and split them into different files, you can do that as well.
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import base from './config/base.config';
import database from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [base, database] *// split your configuration files into separate files*}),
],
})
export class AppModule {}
Let's finally get into actually using the values set in our configuration file(s)!
Using this configuration file as an example:
config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT) || 3000,
pokemonService: {
apiKey: process.env.POKEMEON_KEY,
}
});
You'll need to inject the ConfigService using constructor injection (same as all other services you import), and then you'll have access to the configService.get method as shown below:
feature.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class FeatureService {
constructor(private readonly configService: ConfigService) {}
someFunction(param: string) {
const port = this.configService.get<number>('port');
*// ...*}
someOtherFunction(param: string) {
const pokemonAPIKey = this.configService.get<string>('pokemonService.apiKey');
*// ...*}
}
The above code works great. However, what happens if you forget to set the environment variables in your env file?
If your TSconfig file has the strictNullChecks property set to true, then the above code would show a compiler error because the configService.get method would return undefined if the environment variable was not set.
To solve this, we can leverage a best practice:
Throw an exception during the server startup if you're missing required environment variables.
We'll cover that in the next section.