- Docker-friendly:
e.g., use
.env
files in dev/test, environment variables in production, or any combination - @hapi/joi-based validation of environment variables
- Completely dynamic and customizable determination of the name/location of your
.env
file- means: no code changes to handle unique configs per environment
- Default values (i.e., optional environment variables with default values)
- Trace how an environment variable was resolved (i.e., came from external environment,
.env
file or as a default value) to help debug tricky problems between dev, test, production
- Why another NestJS configuration module?
- API
- How it works
- Schemas
- Module configuration options
- Debug switches
- Full tutorial
- Use with Docker
You can read more about how NestJSConfigManager works if you want. And the simple API is documented here. But this section should get you started quickly.
To install with npm (or run yarn equivalent):
npm install @nestjsplus/config
The package has one global Nest module (ConfigManagerModule
), and one main class (ConfigManager
) that you'll need to work with. The main idea is to create your own ConfigService (in the examples, we'll call it ConfigService
, but you can call it whatever you want). You probably want this in its own module (in the examples,
we'll call it ConfigModule
), which you probably want to be global. You'll then provide your ConfigService
for use in the rest of your application. This approach affords you a great deal of flexibility:
- Centralized setup of the
ConfigurationModule/Service
- Use Dependency Injection to provide the service wherever needed
- Easily override/mock the service for testing
- Future-proof: if you later want to switch to another 3rd party config module, your dependencies are isolated in one place
Following these conventions, your ConfigModule
might look like this:
// src/config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigManagerModule } from '@nestjsplus/config';
import { ConfigService } from './config.service';
@Global()
@Module({
imports: [
ConfigManagerModule.register({
useFile: 'config/development.env',
}),
],
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
This imports and registers the ConfigManagerModule
. The register()
method is how you
configure the module.
In this example, we explicitly provide a full path to the .env
file via the useFile
configuration option.
This is simple, but not terribly flexible. We'll explore more flexible options
below. When using a static file path with useFile
, the path is relative
to the root directory for the project, or the root directory in which the app is
running (in test and production environments). For example, if the app currently
has a structure like:
myproject
├── src
│ └── module1
├── dist
├── node_modules
├── test
├── config
| └── development.env
└── package.json
The above useFile
configuration method would result in the ConfigManagerModule
looking for a dotenv
-formatted file in:
myproject/config/development.env
Your ConfigService
might look like this:
// src/config/config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigManager } from '@nestjsplus/config';
import * as Joi from '@hapi/joi';
@Injectable()
export class ConfigService extends ConfigManager {
// Our custom "schema"
// We supply it to the ConfigManager by extending the
// ConfigManager class and implementing the
// provideConfigSpec() method, which simply returns
// our custom schema
provideConfigSpec() {
return {
DB_HOST: {
validate: Joi.string(),
required: false,
default: 'localhost',
},
DB_PORT: {
validate: Joi.number()
.min(5000)
.max(65535),
required: false,
default: 5432,
},
DB_USERNAME: {
validate: Joi.string(),
required: true,
},
DB_PASSWORD: {
validate: Joi.string(),
required: true,
},
DB_NAME: {
validate: Joi.string(),
required: true,
},
};
}
}
Your ConfigService
(you can choose any name you want) is a derived
class that extends the ConfigManager
class provided by the package. You must
implement the provideConfigSpec()
method. This is where you define your schema.
Read more about schemas here.
With this in place, you can use your ConfigService
anywhere in your project. For example, assuming
a .env
file like:
// myproject/config/development.env
DB_USERNAME=john
DB_PASSWORD=mypassword
DB_NAME=mydb
.
.
.
A service could then use it like this:
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from './config/config.service';
@Injectable()
export class AppService {
private userName: string;
constructor(configService: ConfigService) {
this.userName = configService.get<string>('DB_USERNAME'));
}
getHello(): string {
return `Hello ${this.userName}`;
}
}
And calling getHello()
would return
Hello john
when run with this configuration.
Let's say you have two environments: development and test, and want to set up your
ConfigService
to dynamically locate the .env
file that is appropriate to each environment.
Let's assume that development uses one set of database credentials, and test uses another.
This would be well represented by having two .env
files. Perhaps they're stored in a folder like
myproject/config
(this is just an example; they can be stored wherever you want). The files might look like:
For development:
// myproject/config/development.env
DB_USERNAME=devdbuser
DB_PASSWORD=devdbpass
DB_NAME=devdb
For test:
// myproject/config/test.env
DB_USERNAME=testdbuser
DB_PASSWORD=testdbpass
DB_NAME=testdb
How can we accommodate this dynamic file location without modifying our code?
The useFile()
method of configuration shown above won't work for this. You need
a way to read a different .env
file for each environment. A typical
approach is to use a specific environment variable (typically
NODE_ENV
, though you can choose whatever you want) to indicate what the
active environment is. For example, when
running in development, you'd have NODE_ENV
equal to 'development'
, and
in test, NODE_ENV
's value would equal 'test'
.
Based on this, you can use the useEnv
method of configuring the ConfigManagerModule
.
The useEnv
method is described in the
Module Configuration Options
section, but a simple example is shown below.
To accommodate this requirement, we'd modify the way we register the ConfigManagerModule
as follows, replacing useFile
with useEnv
:
// src/config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigManagerModule } from '@nestjsplus/config';
import { ConfigService } from './config.service';
@Global()
@Module({
imports: [
ConfigManagerModule.register({
useEnv: {
folder: 'config',
},
}),
],
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
If instead of NODE_ENV
we wanted to use an environment variable like
MY_ENVIRONMENT
to signify which environment we're running (e.g.,
MY_ENVIRONMENT
is equal to 'development'
when we're in our development
environment), we'd identify that environment variable using the option envKey
,
as shown below:
// src/config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigManagerModule } from '@nestjsplus/config';
import { ConfigService } from './config.service';
@Global()
@Module({
imports: [
ConfigManagerModule.register({
envKey: 'MY_ENVIRONMENT',
useEnv: {
folder: 'config',
},
}),
],
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
The useEnv
method provides significant flexibility, but more complex structures
require an even more flexible approach. To handle arbitrarily complex
environments, a third method, useFunction
, is available to write custom
JavaScript code to generate the appropriate path and filename dynamically.
This is covered in
Using a custom function.
See Changelog for more information.
Contributions welcome! See Contributing.
- John Biundo (Y Prospect on Discord)
Licensed under the MIT License - see the LICENSE file for details.