Greetings, today we will discuss about environment variables in NestJs, specifically using Nx.
As you might know, Nx autogenerates a lot of boilerplate code for us. It does the same with the environment files.
After you create a new project you can see that you have 2 files environment
and environment.prod.ts
that are waiting for you.
These files are basically the same, the .prod
file will replace the other one on build if the production flag is set.
All this magic can be seen inside project.json
Now, if we would be able to simply set everything in there, this article would be pointless. Usually, we want to have the secrets of the application… secret. For the local dev environment, it might be fine to save in the repo your config, but for production, it is definitely not.
So, to simplify a lot of things I propose a mixed approach. For local, we set everything is environment.ts
and for production(and others) we set only nonsecret things in environment.prod.ts
and the rest as environment variables.
Let's get to work!
Configuration
First, we will need the ConfigModule available in the app so proceed to run npm install --save @nestjs/config class-validator class-transformer
Then in the environment
folder (or a different one) you can create env-config.ts
which will contain
/** Environment variables take precedence */
export function getEnvConfig() {
const envVariables = parseEnvVariables();
return mergeObject(env, envVariables);
}
function parseEnvVariables() {
const envVariables: DeepPartial<IEnvironment> = {};
const env = process.env;
if (env) {
if (env.PRODUCTION) envVariables.production = env.PRODUCTION === 'true';
if (env.PORT) envVariables.port = parseInt(env.PORT, 10);
if (env.BASE_URL) envVariables.baseUrl = env.BASE_URL;
if (env.GLOBAL_API_PREFIX) envVariables.globalApiPrefix = env.GLOBAL_API_PREFIX;
envVariables.database = {};
if (env.DATABASE_HOST) envVariables.database.host = env.DATABASE_HOST;
if (env.DATABASE_PORT) envVariables.database.port = parseInt(env.DATABASE_PORT, 10);
if (env.DATABASE_DATABASE) envVariables.database.database = env.DATABASE_DATABASE;
if (env.DATABASE_USERNAME) envVariables.database.username = env.DATABASE_USERNAME;
if (env.DATABASE_PASSWORD) envVariables.database.password = env.DATABASE_PASSWORD;
}
return envVariables;
}
function mergeObject(obj1: Record<string, any>, obj2: Record<string, any>) {
for (const key in obj2) {
if (obj2.hasOwnProperty(key)) {
if (typeof obj2[key] === 'object') {
mergeObject(obj1[key], obj2[key]);
} else {
obj1[key] = obj2[key];
}
}
}
return obj1;
}
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
Let's explain, the getEnvConfig
function will be called by the ConfigModule
at start-up and will retrieve both the environment variables and the environment object from environment.ts
. Then we will merge all the properties together and return the object. The environment variables have priority in the merge.
You might have noticed some interfaces too. Better typing is always good to have so we need to create env.interface.ts
in the same folder containing
export interface IEnvironment {
production: boolean;
port: number;
baseUrl: string;
globalApiPrefix: string;
database: IDatabaseEnvironment;
}
export interface IDatabaseEnvironment {
host: string;
port: number;
database: string;
username: string;
password: string;
}
At this point we are almost done.
All we need to do more is to add the interface above in the environment.ts
and environment.prod.ts
My files look like this:
// environment.ts
import { IEnvironment } from './env.interface';
export const env: Partial<IEnvironment> = {
production: false,
globalApiPrefix: 'api',
port: 3333,
baseUrl: 'http://localhost',
database: {
host: 'localhost',
port: 5432,
database: 'postgres',
username: 'postgres',
password: 'password',
},
};
// environment.prod.ts
export const env: Partial<IEnvironment> = {
production: true,
};
The other single thing remaining to do is registering the ConfigModule
, so open app.module.ts
and add
ConfigModule.forRoot({
load: [getEnvConfig],
isGlobal: true,
cache: true,
}),
This should get you going. Let's talk now about validation.
Validation
One way is to you Joi, but it is not recommended for Node users above v17.
Personally I used a custom validation.
Create in the same environment
folder another file env-validator
.
import { plainToClass } from 'class-transformer';
import { IsBoolean, IsNumber, IsObject, IsString, validateSync } from 'class-validator';
import { getEnvConfig } from './env-config';
import { IDatabaseEnvironment, IEnvironment } from './env.interface';
class DatabaseEnvironment implements IDatabaseEnvironment {
@IsString()
host: string;
@IsNumber()
port: number;
@IsString()
database: string;
@IsString()
username: string;
@IsString()
password: string;
}
class Environment implements IEnvironment {
@IsBoolean()
production: boolean;
@IsNumber()
port: number;
@IsString()
baseUrl: string;
@IsObject()
database: DatabaseEnvironment;
@IsString()
globalApiPrefix: string;
}
export function envValidation() {
const config = getEnvConfig();
const validatedConfig = plainToClass(Environment, config, { enableImplicitConversion: true });
const errors = validateSync(validatedConfig, { skipMissingProperties: false });
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
As a short explanation, we get the environment variables using the function created at the beginning, then using a transformer function we create the objects above which contains the validators needed. Then we validate each property. If the validation fails the app will not run.
Now we only need to trigger the validation.
Let's go to app.module.ts
and change
ConfigModule.forRoot({
load: [getEnvConfig],
isGlobal: true,
cache: true,
}),
to
ConfigModule.forRoot({
load: [getEnvConfig],
isGlobal: true,
cache: true,
validate: envValidation,
})
Usages
This configuration has many usages. For example you can dynamically change the database based on the environment, or the port.
Let's check one of the usages. Particularly the port.
We can update main.ts
to use the ConfigService
and get the env variable.
So from something like
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
await app.listen(3000);
}
we can get
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config: ConfigService = app.get(ConfigService);
const globalApiPrefix = config.get<IEnvironment['globalApiPrefix']>('globalPrefix') ?? 'api';
app.setGlobalPrefix(globalApiPrefix);
const port = config.get<IEnvironment['port']>('port') ?? 3333;
await app.listen(port);
}
Thanks for reading!
Top comments (5)
env is undefined? should this take env as an input?
or should it be
process.env
instead ofenv
?env
is an exported constant object that you can set. It is useful mainly for development but it can be used in production too.In this article you can see it in this section
Those file are then taken by nx and depending on the configuration (dev or prod) only one of them will be added in the final build with the name
environment.ts
so you don't have to worry about imports.Ah, interesting. I get an error if i don't import one of them. (NX setup/config issue?)
You totally have to import one of the from the path ending in just "emvironment". When you use production mode it will automatically use "environment.prod" instead
you rock!