Managing multiple environments in NestJS
Tags: nestjs, environment, javascript
Published: 2021-09-22 16:47:32

So, recently I started working on a new startup and each time I do, I try to adopt a new technology be it language or framework <insert the name here>. (this is not always recommended, in this case I have previous experience with NestJS)

This time around I chose to adopt NestJS. Have used it before for pet projects and found it really fun so I thought why not to use it as the backend for my new startup? Felt like a no-brainer.

# The problem

As this is not my first rodeo with startups, I actually take time to set up the backend properly instead of being in an MVP rush mode. One of the things that needed configuration early on, was **the separation of environment variables between different modes**.

> e.g development, test, staging, and production

Looking at the [docs](https://docs.nestjs.com/techniques/configuration) there is no real suggestion on how to do that but it gives you breadcrumbs here and there on how to achieve such a thing by putting the pieces together.

So here I am documenting how I did it so you don't have to waste more time on it. Ready? Let's go.

## Step 1

Create the following structure in the root of your NestJS app.

![Screenshot 2021-08-16 at 7.37.13 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629131835696/0MqiMOfZ8.png)

## Step 2 - Initializing ConfigModule

Open up your `app.module` and write the following

```javascript
import { ConfigModule } from '@nestjs/config';

// ...skipping irrelevant code

@Module({
  imports: [
    ConfigModule.forRoot(), 
    PrismaModule,
    ProductsModule,
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})

// ...skipping irrelevant code
```

if we don't pass any `options` to the `ConfigModule` by default it is looking for a .env file in the root folder but it cannot distinguish between environments. Let's move onto the next steps where we make the `ConfigModule` smarter in where to look and what to load

## Step 3 - Populating the development.env file

Let's populate the `development.env` file as a first step towards creating separate environments. 

```
JWT_SECRET=luckyD@#1asya92348
JWT_EXPIRES_IN=3600s
PORT=3000
```

## Step 4 - Populating the `configuration` file

`configuration.ts` - its main purpose is to create an object (of any nested level) so that you can group values together and make it easier to go about using it. 

Another benefit is to provide defaults in case the env variables are undefined and on top of that you can typecast the variable as it's done for the port number below.

```javascript
// configuration.ts

export const configuration = () => ({
  NODE_ENV: process.env.NODE_ENV,
  port: parseInt(process.env.PORT, 10) || 3001,
   jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN,
  }
});
```

Then let's pass options to the `ConfigModule` to use this configuration file like so:

```

import { configuration } from '../config/configuration'; // this is new

// ... skipping irrelevant code

@Module({
  imports: [
    ConfigModule.forRoot({ 
       envFilePath: `${process.cwd()}/config/env/${process.env.NODE_ENV}.env`,
       load: [configuration] 
    }), 
    PrismaModule,
    ProductsModule,
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})

// ...skipping irrelevant code
```
We have now used two options to configure the `ConfigModule`. 

* load

This should be pretty self-explanatory, that it loads the configuration file we are giving it and does all the goodies mentioned above.

* envFilePath

We are pointing the module (underneath its using the dotenv package) to read an .env file based on the `process.env.NODE_ENV` environment variable. 

> `process.cwd()` is a handy command that provides the current working directory path

BUT we are just now loading the variables, how do you expect the module to make use of the `process.env.NODE_ENV` variable before the env variables are loaded?! 

Well, read more on the next step!

## Step 5 - Initializing the NODE_ENV env variable

First of all, what is the NODE_ENV variable for? Well, it's a practice used by devs to denote which environment they are using. 

In short, NODE_ENV lets the app know if it should run in the development, production, you-name-it environment by looking at its value. 


There are actually many ways to go about loading env variables, and one of them is to set the variable inline to the execution script like so:

```
// package.json

"scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "NODE_ENV=development nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "NODE_ENV=production node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
  },
```

> Notice the `NODE_ENV=development` and `NODE_ENV=production` above.

When we execute the script using one e.g `npm run start:dev` it will actually set the variable and will be accessible in your NestJS app. Cool, this gives an answer to the question we had above.

> Windows users must install `cross-env` package as windows doesn't support this way of loading variables and alter the commands like so `"start:dev": "cross-env NODE_ENV=development nest start --watch"`

## Step 6 - Usage

We now have two methods of reaching the values of the env variables

### Method 1

As seen above we can make use of the process.env.<name> to access the variables. However, this has some drawbacks in terms of accessing env variables during module instantiation so be mindful of that.

### Method 2

Using the `ConfigService` to access the variables. Setting up the `ConfigModule` now gives us access to its service which consequently gives us access to the variables

#### Example

```javascript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ConfigService } from '@nestjs/config';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService, private configService: ConfigService) {}

  @Get()
  getHello(): string {
    console.log(this.configService.get<string>('jwt.secret')
  }
}

```

## Step 7 - Update .gitignore

If you do a `git status` you should notice that the `development.env` file is being watched and will be committed. While that is somewhat OK as long as you don't use the same values for example in the `production.env` lets update .gitignore to ignore `.env` files:

```
// .gitignore

// add at the bottom

**/*.env
!config/env/development.env

``` 

What it says here, is to ignore all `.env` files except for `development.env`

## (**BONUS**) - Validating the env variables 

Now we have come full circle but we can go one step further to ensure that our variables are in the correct type and loaded.

#### Step 1 - Install `joi`

This library will do the heavy lifting of validating our env variables by comparing them against a `schema` we provide.

```
npm install joi

OR

yarn add joi
```

#### Step 2 - Populate validation.ts

```javascript
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string().valid(
    'development',
    'production',
    'test',
    'provision',
  ),
  JWT_SECRET: Joi.string().required(),
  JWT_EXPIRES_IN: Joi.string().required(),
  PORT: Joi.number().default(3000),
});
```

So what we did above was to make sure that the NODE_ENV is one of the mentioned strings, the JWT_* variables are `strings` and `required`, and we require the `port` to be a number and have a default value (hence why we don't `required()` a value to be present)

> Notice that the validation schema naming must be exactly like it's in the `.env` file and NOT how you wrote the names in `configuration.ts`

#### Step 3 - Update options in `ConfigModule`

```javascript
import { validationSchema } from '../config/validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `${process.cwd()}/config/env/${process.env.NODE_ENV}.env`,
      load: [configuration],
      validationSchema,
    }),
    PrismaModule,
    ProductsModule,
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
```

So here we imported and provided the `validationSchema` to the module. 

> Exercise: Try setting up NODE_ENV something else than the four values defined in the validation schema and see what happens


## (**BONUS 2**) - Avoid the need of importing the config module everywhere

There is a handy option to avoid having to import the config module in every module that is being used which is pretty neat. Its called `isGlobal` and below you can find how it's used

```javascript
@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `${process.cwd()}/config/env/${process.env.NODE_ENV}.env`,
      isGlobal: true,
      load: [configuration],
      validationSchema,
    }),
    PrismaModule,
    ProductsModule,
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
```

# Summary

You have set up a flexible way of setting up your env variables for each environment in a non-complicated manner while also maintaining type and value integrity by validating the env variables against a schema.

I hope you found this useful and if you want to keep in touch you can always find me on [Twitter](https://twitter.com/9pitops).