Ways to validate environment configuration in a forFeature Config in NestJs
Tags: nestjs, typescript, node, backend
Published: 2022-03-06 15:31:10

- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [Methods to process configuration files](#methods-to-process-configuration-files)
- [Schema validation](#schema-validation)
- [Preparing our environment](#preparing-our-environment)
- [Using Joi](#using-joi)
 - [Types](#types)
 - [Avoid code duplication](#avoid-code-duplication)
     - [Creating a new interface](#creating-a-new-interface)
     - [Utility power](#utility-power)
 - [Usage with multiple configuration modules](#usage-with-multiple-configuration-modules)
- [Using a custom validate function](#using-a-custom-validate-function)
 - [Custom validator per factory function](#custom-validator-per-factory-function)
     - [Extract the validate function](#extract-the-validate-function)
- [Using a base class](#using-a-base-class)
- [Conclusion](#conclusion)

## Introduction

Is normal and a best practice to have a `.env` file to change configurations quickly based on the environment variable.

Thank God that **NestJS** provides a `ConfigModule` that exposes a `ConfigService` which loads an `.env` file. Internally, this uses [dotenv](https://github.com/motdotla/dotenv) to load the variables from the file into `process.env`.

To set up the `ConfigModule` is pretty straightforward if you follow the official documentation.

> You can read more about NestJS Configuration [here](https://docs.nestjs.com/techniques/configuration).

## Prerequisites
To following along, ensure you have basic knowledge and experience with:

- [NodeJS](https://nodejs.org/en/) - Is a JavaScript runtime built on Chrome's V8 JavaScript engine.
- [NestJS](https://nestjs.com/) - A progressive Node.js framework for building efficient, reliable and scalable server-side applications.
- [TypeScript](https://www.typescriptlang.org/) - Is JavaScript with syntax for types.
- [Environment Variables](https://en.wikipedia.org/wiki/Environment_variable) - Is a variable whose value is set outside the program.

## Methods to process configuration files

You can process your file in the root module `AppModule` with the `forRoot()` method. The official documentation already shows how to do validations using this way.

If you have a more complex project structure, with feature-specific configuration files, the `@nestjs/config` package provides a feature called [partial registration](https://docs.nestjs.com/techniques/configuration#partial-registration), which references only the configuration files associated with each feature module. By using the `forFeature()` method within a feature module, you can load just a few environment variables to a module.

The documentation doesn't mention how to apply validations if you are using the `forFeature()` method. This will be our focus in this article.

## Schema validation

The `@nestjs/config` package enables two different ways to do validations:

1. Using [Joi](https://github.com/sideway/joi), a data validator for JavaScript.
2. Custom validate function using `class-transformer` and `class-validator` packages, which takes environment variables as an input.

We are gonna see each one with examples.

## Preparing our environment

Install the required dependency:

```bash
npm i --save @nestjs/config
```

The `.env` file that we are gonna use is as follows:

```dosini
NODE_ENV=development
PORT=3000
```

Let's define a [configuration namespace](https://docs.nestjs.com/techniques/configuration#configuration-namespaces) to load multiple custom environment variables:

```typescript
import { registerAs } from '@nestjs/config';

export default registerAs('my-app-config-namespace', () => ({
  nodeEnv: process.env.NODE_ENV,
  port: parseInt(process.env.PORT)
}));
```

As the docs says, inside this `registerAs()` factory function, the `process.env` object will contain the fully resolved environment variable key/value pairs.

Lastly, let's create a module with the following:

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

// This is our factory function from the step before.
import appConfig from './configuration';

@Module({
  imports: [
    ConfigModule.forFeature(appConfig)
  ],
  providers: [],
  exports: [],
})
export class AppConfigModule {}
```

The `forFeature()` method doesn't have the property `validationSchema` just like the `forRoot()` has. This property enables you to provide a Joi validation. It also does not have the property `validate` where you can pass a custom validate function.

At this moment, I was lost and I didn't know what to do. Let's continue...

## Using Joi

Install the required dependency:

```bash
npm install --save joi
```

Let's grab our factory function from before and apply some validations:

```typescript
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';

export default registerAs('my-app-config-namespace', () => {
  // Our environment variables
  const values = {
    nodeEnv: process.env.NODE_ENV,
    port: parseInt(process.env.PORT),
  };

  // Joi validations
  const schema = Joi.object({
    nodeEnv: Joi.string().required().valid('development', 'production'),
    port: Joi.number().required(),
  });

  // Validates our values using the schema.
  // Passing a flag to tell Joi to not stop validation on the
  // first error, we want all the errors found.
  const { error } = schema.validate(values, { abortEarly: false });

  // If the validation is invalid, "error" is assigned a
  // ValidationError object providing more information.
  if (error) {
    throw new Error(
      `Validation failed - Is there an environment variable missing?
        ${error.message}`,
    );
  }

  // If the validation is valid, then the "error" will be
  // undefined and this will return successfully.
  return values;
});
```

I hope the comments helps to understand the code.

If we delete our `.env` file or if we pass invalid values, we will see in the console something like this:

> Error: Validation failed - Is there an environment variable missing?
> "nodeEnv" is required. "port" must be a number

### Types

If you have noticed, we are not using any types. Let's create an interface in a new file:

```typescript
export interface IAppConfig {
  nodeEnv: string;
  port: number;
}
```

Now we can apply it to our factory function:

```typescript
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { IAppConfig } from './interface';

// Factory function now has a return type
export default registerAs('my-app-config-namespace', (): IAppConfig => {
  // Object with an interface
  const values: IAppConfig = {
    nodeEnv: process.env.NODE_ENV,
    port: parseInt(process.env.PORT),
  };

  // Joi uses generics that let us provide an interface in the
  // first position. In the second position, we provide -true-
  // to tell Joi that every key of the interface is mandatory
  // to be present in the schema.
  const schema = Joi.object<IAppConfig, true>({
    nodeEnv: Joi.string().required().valid('development', 'production'),
    port: Joi.number().required(),
  });

  // ...

  // ..

  return values;
});
```

For example, if we delete `port` from our `schema` object, we will see an error like this:

![type error screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58kavi8czzm4uqonlxjm.png)

👍 Nice job!

### Avoid code duplication

Imagine that we have a lot of **configuration modules**, each one with a **namespace**, I'm too lazy to duplicate all of the code from before on each file. Besides, this is a bad practice.

In addition, is very hard for me to write the same property name twice, inside our `values` and `schema` object from before. 

```typescript
const values = {
  nodeEnv: ...,
  port: ...
};

const schema = Joi.object({
  nodeEnv: ...,
  port: ...,
});
```

🤔 I cannot live happy with that.

#### Creating a new interface

What I would love to have:

1. Write property names just once
2. Tell what is its value from the environment variables
3. Tell what are its Joi validation rules
4. Keep the type feature for safety

We can come up with this technique:

```typescript
Record<keyof IAppConfig, { value: unknown; joi: Schema }>
```

We are using the [Keyof Type Operator](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html) and the type `Schema` that comes from the Joi library that represents validation rules.

Example of use:
```typescript
const configs: Record<keyof IAppConfig, { value: any; joi: Schema }> = {
  nodeEnv: {
    value: process.env.NODE_ENV,
    joi: Joi.string().required().valid("development", "production"),
  },
  port: {
    value: parseInt(process.env.PORT),
    joi: Joi.number().required(),
  },
};
```

😱 That is so cool...

But, wait a minute. We cannot pass to Joi that thing as an input!... and you are right, there is more pending work for us. 😂

We need to figure out a way to have an object with Joi's needs, and another object to return what the factory function's needs. Each object has the same properties but with different values.

```typescript
/*
  Result example;
  [
    { propName: ... },
    { propName: ... }
  ]
*/
const joiSchemaArr: SchemaMap<IAppConfig>[] = Object.keys(configs).map(
  (key) => {
    return {
      [key]: configs[key].joi, // Keep an eye on this
    };
  }
);

/*
  Result example;
  {
    propName: ...,
    propName: ...
  }
*/
const joiSchema: SchemaMap<IAppConfig> = Object.assign({}, ...joiSchemaArr);

const schema = Joi.object(joiSchema);
```

Okay, now we have what Joi's needs. Only one thing is left, the factory function. Thinking on repeating this code again to extract the `value` property instead of te `joi` property from our interface, the laziness came on me again. 😂

#### Utility power

Let's create an utility file called `joi-util.ts` that help us to avoid duplicating the code on every configuration file without necessity. In addition, I will **delegate the responsibility to throw the error** to keep my factory function as clean as possible. Also, let's use some `types` and [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) too. 💪🏻

```typescript
import * as Joi from 'joi';
import { Schema, SchemaMap } from 'joi';

interface ConfigProps {
  value: unknown;
  joi: Schema;
}

export type JoiConfig<T> = Record<keyof T, ConfigProps>;

/**
 * Utility class to avoid duplicating code in the configuration of our namespaces.
 */
export default class JoiUtil {
  /**
   * Throws an exception if required environment variables haven't been provided
   * or if they don't meet our Joi validation rules.
   */
  static validate<T>(config: JoiConfig<T>): T {
    const schemaObj = JoiUtil.extractByPropName(config, 'joi') as SchemaMap<T>;
    const schema = Joi.object(schemaObj);
    const values = JoiUtil.extractByPropName(config, 'value') as T;

    const { error } = schema.validate(values, { abortEarly: false });
    if (error) {
      throw new Error(
        `Validation failed - Is there an environment variable missing?
        ${error.message}`,
      );
    }

    return values;
  }

  /**
   * Extract only a single property from our configuration object.
   * @param config    Entire configuration object.
   * @param propName  The property name that we want to extract.
   */
  static extractByPropName<T>(
    config: JoiConfig<T>,
    propName: keyof ConfigProps,
  ): T | SchemaMap<T> {
    /*
      Result example;
      [
        { propName: ... },
        { propName: ... }
      ]
     */
    const arr: any[] = Object.keys(config).map((key) => {
      return {
        [key]: config[key][propName],
      };
    });

    /*
      Result example;
      {
        propName: ...,
        propName: ...
      }
     */
    return Object.assign({}, ...arr);
  }
}
```

> I'm not so good on advanced types, this can be improved.

Did you notice anything new on our validate function? Yes, a thing called `as` in TypeScript. It's a [Type Assertion](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) and let us to help the compiler to know which is the type that we are expecting from our `extractByPropName()` function.

I know this file is long, but no worries... you won't have to repeat it never in your life.

Example of use:

```typescript
import { registerAs } from '@nestjs/config';
import { IAppConfig } from './interface';
import * as Joi from 'joi';
import JoiUtil, { JoiConfig } from '../joi-util';

export default registerAs('my-app-config-namespace', (): IAppConfig => {
  const configs: JoiConfig<IAppConfig> = {
    nodeEnv: {
      value: process.env.NODE_ENV,
      joi: Joi.string().required().valid('development', 'production'),
    },
    port: {
      value: parseInt(process.env.PORT),
      joi: Joi.number().required(),
    },
  };

  return JoiUtil.validate(configs);
});
```

😈 That is what I'm talking about, awesome!

### Usage with multiple configuration modules

We now have a new business requirement and we need to communicate to a database. Let's create another configuration module with specific environment variables.

First, define the environment variables:

```dosini
DATABASE_USERNAME=root
DATABASE_PASSWORD=123456789
DATABASE_NAME=mydb
DATABASE_PORT=3306
```

The configuration namespace to load multiple custom environment variables:

```typescript
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import JoiUtil, { JoiConfig } from '../joi-util';

interface IDatabaseConfig {
  username: string;
  password: string;
  database: string;
  port: number;
}

export default registerAs('database-config-namespace', (): IDatabaseConfig => {
  const configs: JoiConfig<IDatabaseConfig> = {
    username: {
      value: process.env.DATABASE_USERNAME,
      joi: Joi.string().required(),
    },
    password: {
      value: process.env.DATABASE_PASSWORD,
      joi: Joi.string().required(),
    },
    database: {
      value: process.env.DATABASE_NAME,
      joi: Joi.string().required(),
    },
    port: {
      value: parseInt(process.env.DATABASE_PORT),
      joi: Joi.number().required(),
    },
  };

  return JoiUtil.validate(configs);
});
```

> I've just add a few variables, in real life you will have more.

Lastly, let's create a module with the following:

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

// This is our factory function from the step before.
import databaseConfig from './database-configuration';

@Module({
  imports: [
    ConfigModule.forFeature(databaseConfig)
  ],
  providers: [],
  exports: [],
})
export class DatabaseConfigModule {}
```

You will repeat these steps on every configuration module and that's it. 🙂

## Using a custom validate function

To use this way, we need to install `class-transformer` and `class-validator` packages, which takes environment variables as an input.

```bash
npm i --save class-transformer class-validator
```

The [documentation](https://docs.nestjs.com/techniques/configuration#custom-validate-function) shows an example about this, but it's intended to be used with the `forRoot()` method. Let's see how can we use this way by using the `forFeature()` method.

### Custom validator per factory function

Let's define a [configuration namespace](https://docs.nestjs.com/techniques/configuration#configuration-namespaces) to load multiple custom environment variables:

```typescript
import { registerAs } from '@nestjs/config';
import { IAppConfig } from './interface';

export default registerAs('my-app-config-namespace', (): IAppConfig => ({
    nodeEnv: process.env.NODE_ENV,
    port: parseInt(process.env.PORT),
  }),
);
```

Now, we can take the same example from the documentation and adjust it to our requirements. Let's create a new file called `app-env.validation.ts` with the following:

```typescript
import { plainToClass } from 'class-transformer';
import { IsEnum, IsNumber, validateSync } from 'class-validator';

enum Environment {
  Development = 'development',
  Production = 'production',
}

class AppEnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToClass(
    AppEnvironmentVariables,
    config,
    { enableImplicitConversion: true },
  );
  const errors = validateSync(validatedConfig, { skipMissingProperties: false });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}
```

To apply the validate function, is like follows:

```typescript
import { registerAs } from '@nestjs/config';
import { IAppConfig } from './interface';

// This is our custom validate function from the step before.
import { validate } from './app-env.validation';

export default registerAs('my-app-config-namespace', (): IAppConfig => {

  // Executes our custom function
  validate(process.env);

  // If all is valid, this will return successfully
  return {
    nodeEnv: process.env.NODE_ENV,
    port: parseInt(process.env.PORT),
  };
});
```

If we delete our `NODE_ENV` and `PORT` variables from the `.env` file, we will see:

```
Error:
An instance of AppEnvironmentVariables has failed the validation:
 - property NODE_ENV has failed the following constraints: isEnum 
An instance of AppEnvironmentVariables has failed the validation:
 - property PORT has failed the following constraints: isNumber 
```

You need to do custom validate functions for every factory function with a namespace.

🤔 Mmm... this smells like a code duplication of the custom validate function! Well, this time is natural because each one will have different rules.

Looking at the file `app-env.validation.ts` we have created, we can see a repetitive part that we can reuse across the project, the `validate()` function.

```typescript
export function validate(config: Record<string, unknown>) {
  ...
}
```

#### Extract the validate function

Let's create a new file called `validate-util.ts`:

```typescript
import { plainToClass } from 'class-transformer';
import { validateSync } from 'class-validator';
import { ClassConstructor } from 'class-transformer/types/interfaces';

export function validateUtil(
  config: Record<string, unknown>, 
  envVariablesClass: ClassConstructor<any>
) {
  const validatedConfig = plainToClass(
    envVariablesClass,
    config,
    { enableImplicitConversion: true },
  );
  const errors = validateSync(validatedConfig, { skipMissingProperties: false });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}
```

Our old `app-env.validation.ts` will look like:

```typescript
import { IsEnum, IsNumber } from 'class-validator';

enum Environment {
  Development = 'development',
  Production = 'production',
}

export class AppEnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;
}
```

Lastly, our factory function will look like:

```typescript
import { registerAs } from '@nestjs/config';
import { IAppConfig } from './interface';

// This is our class that uses "class-validator" decorators
import { AppEnvironmentVariables } from './app-env.validation';

// Our new utility to apply the validation process
import { validateUtil } from '../validate-util';

export default registerAs('my-app-config-namespace', (): IAppConfig => {

  // Executes our custom function
  validateUtil(process.env, AppEnvironmentVariables);

  // If all is valid, this will return successfully
  return {
    nodeEnv: process.env.NODE_ENV,
    port: parseInt(process.env.PORT),
  };

});
```

The core of the process for validation is extracted and does not need to be repeated anymore. Also, our `AppEnvironmentVariables` is cleaner and easy to understand and maintain. 😀

### Using a base class

Another way to apply validations is by using a `base class`. All the credit goes to [Darragh ORiordan](https://www.darraghoriordan.com/contact/) and his article called [How to validate configuration per module in NestJs](https://www.darraghoriordan.com/2021/10/10/validate-configuration-module-feature-nestjs/). I encourage you to check it out!.

## Conclusion

I've tried to recap in a single place all the ways you can do validations when using the `forFeature()` method in NestJs.

I hope you liked my article and see you soon with more advices like this.