DEV Community

Cover image for Easy Steps to Generate a Config Module in NestJS
Ruben Alvarado
Ruben Alvarado

Posted on

Easy Steps to Generate a Config Module in NestJS

When I started with NestJS, I didn't understand modules, interceptors, guards, and other advanced techniques the framework offers. As a result, I built insecure applications with poor practices. Over time, I learned the hard way which patterns truly matter for production-grade applications.

After several years working with NestJS, I've collected capabilities I want to share. One is creating a custom config module where you can declare, load, and validate environment variables. After reading this post, you'll build more robust, production-ready applications.

Setting up the project

First, I'll install the Nest CLI globally to access the framework's capabilities. I like using the CLI to boost my productivity when working with Nest.

npm install -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

With the CLI installed, I can create a new Nest project using the command nest new. I prefer working with pnpm as my package manager, but you can choose whichever works best for you. I'll name my project config-module-example.

nest new config-module-example
Enter fullscreen mode Exit fullscreen mode

Now I'll install the config service from Nest.

pnpm i @nestjs/config
Enter fullscreen mode Exit fullscreen mode

With the project set up, it's time to start building. I'll begin by creating a new module.

Creating the Module

As a best practice, I create a common folder for configurations, database schemas, definitions, validations, and other non-business logic utilities. Let's start there.

Using the CLI, I'll run this command:

nest g mo common/config
Enter fullscreen mode Exit fullscreen mode

This command creates a config module inside the common folder and a config.module.ts file where I'll define my custom configuration module—don't confuse it with the built-in one.

To make my module global, I'll add the @Global decorator so I can use it throughout the entire application. I also need to import the @nestjs/config package but rename it as NestConfigModule to avoid confusion with my own config module.

import { ConfigModule as NestConfigModule } from '@nestjs/config';
Enter fullscreen mode Exit fullscreen mode

Now I'll configure how the module behaves. Since I'm working with .env files, I need to tell the Nest container where to find them and enable caching. I also need to export this module so it can be used as a provider.

With these configurations in place, my module looks like this:

@Global()
@Module({
  imports: [
    NestConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [
        `.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`,
      ],
      cache: true,
    }),
  ],
  exports: [NestConfigModule],
})
export class ConfigModule {}
Enter fullscreen mode Exit fullscreen mode

Pay attention to the envFilePath property—this tells the module to use the environment-specific .env file if NODE_ENV is present, or the default one if not. With my config module defined, I'll now define the types for working.

Defining Configuration Types

For this example, I'll declare some basic environment variables. You can extend them as needed.

Under the common folder, I'll create a types folder with a file called app.types.ts. Here I'll define the AppConfig type:

export type AppConfig = {
  port: number;
  nodeEnv: 'development' | 'production' | 'test' | 'staging';
}; 
Enter fullscreen mode Exit fullscreen mode

With the types defined, I can now set up the application's configuration.

Working with Configurations

With the types defined, I'll now create the configuration file. Inside the config folder, I'll create a configurations folder and add a file called app.config.ts. I need to import the registerAs function from @nestjs/config. This function registers a configuration namespace that I can reference later. Here's how it looks:

export default registerAs(
  'app',
  (): AppConfig => ({
    port: parseInt(process.env.PORT || '3000', 10),
    nodeEnv: (process.env.NODE_ENV || 'development') as
      | 'development'
      | 'production'
      | 'test'
      | 'staging',
  }),
);
Enter fullscreen mode Exit fullscreen mode

I'm using registerAs with two arguments: the namespace token 'app' and a factory function that returns the AppConfig object. This maps my environment variables to a type-safe configuration. But what happens if someone loads a corrupt .env file with invalid variables? To prevent this, the next step is to define and validate a schema.

Validating Schemas with Joi

With the configuration in place, I need to ensure the .env file doesn't have corrupt or invalid variables. To do this, I'll create a schema with validations using Joi.

First, I'll install Joi:

pnpm i joi
Enter fullscreen mode Exit fullscreen mode

Next, I'll create a validations folder under my config module to hold my schemas. Using Joi, I'll define the validation schema:

export const appValidationSchema = Joi.object({
  PORT: Joi.number().default(3000),
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .default('development'),
});
Enter fullscreen mode Exit fullscreen mode

The schema validates that PORT is a number and NODE_ENV is one of the allowed environment values, with sensible defaults for both. With all the pieces ready, it's time to wire everything together and make this configuration work.

Wiring It All Together

Now I'll connect all the pieces. I'll return to the config.module.ts file and add the configuration loader and validation schema:

@Global()
@Module({
  imports: [
    NestConfigModule.forRoot({
      isGlobal: true,
      load: [appConfig],
      validationSchema: appValidationSchema,
      envFilePath: [
        `.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`,
      ],
      cache: true,
    }),
  ],
  exports: [NestConfigModule],
})
export class ConfigModule {}
Enter fullscreen mode Exit fullscreen mode

The load property tells the module which configuration files to load, and validationSchema ensures the environment variables are valid before the app starts. With the module complete, let's test that it works.

Testing the Configuration works

To test the configuration, I'll log the port and environment at startup. In my main.ts file, I'll inject the ConfigService and retrieve the configuration values:

const configService = app.get(ConfigService);

const port = configService.get<number>('app.port', { infer: true }) as number;

const nodeEnv = configService.get<string>('app.nodeEnv', { infer: true }) as string;

if (nodeEnv === 'development') {

    console.log(`Application is running in ${nodeEnv} mode`);

    console.log(`Listening on port ${port}`);

}
Enter fullscreen mode Exit fullscreen mode

Before running the application, I'll create a .env file with the environment variables:

PORT=3000

NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode

Now I'll run pnpm run start:dev. If everything is configured correctly, the application will start and display the development logs.

Final Thoughts

Creating a custom configuration module in NestJS is essential for building robust, maintainable applications. In this article, I've shown you how to structure environment variables, validate them with Joi schemas, and organize them in a way that scales with your project.

This setup prevents the configuration mistakes I made early in my NestJS journey. Type-safe configurations with validation catch errors before they reach production, making your applications more reliable and easier to maintain.

You can find the complete code in my Github repository: https://github.com/RubenOAlvarado/config-module-example

Happy coding—see you in the next one!

Top comments (0)