Tools
Tools: Developing a Tailored Config Module for NestJS Applications
2026-02-19
0 views
admin
Why a custom Config Module? ## Creating the module ## Defining configuration namespaces (app, db) ## Validating environment variables with Joi ## Testing it quickly ## Wrapping Up Have you ever struggled with more environment variables than actual features? DATABASE_URL, SUPABASE_URL, JWT_SECRET, a couple of flags for local vs production, and maybe some “temporary” variables you promise you’ll clean up later. If you read process.env directly everywhere, the codebase becomes fragile fast: One typo silently breaks your connection. One missing variable makes the app crash at runtime. You end up debugging configuration instead of shipping. What if there's a better, cleaner, and more professional way to handle this mess? In this post, we'll build a small, type-safe, validated Config Module that will serve as the foundation for the rest of this series. Nest already provides @nestjs/config, and it’s great. The issue is that most tutorials stop at “install it and call it a day”. For a production-grade API, we want a little more: One place to load and validate environment variables Clear namespaces like app, and db (for now) Type inference so configuration access is safe and discoverable Fail fast validations (before the app starts) This is especially important in our Personal Finance API because our next steps depend on stable configuration: Drizzle needs a valid Postgres connection string Local and production environments must behave predictably We’ll create a module that is global and acts as the single source of truth for configuration. Generate a config module (choose your own path): Now set up the module using Nest’s ConfigModule, but keep it wrapped behind your own module. This already gives us: Global config (no need to import it everywhere) Environment-specific .env loading Caching for performance Instead of scattering variable names throughout the codebase, we’ll create configuration “namespaces”. This keeps things organized and makes future posts easier to follow. Example: app config. You can do the same for db. For this series, the important part is that by the time we connect Drizzle, we can read something like: Type safety is nice, but validation is what prevents bad configuration from reaching production. We’ll use Joi to validate env variables before the app boots. Create a validation schema: Now wire everything together: Create a .env file with the variables you defined and boot the app. If any variable is missing or invalid, Nest will fail fast and tell you exactly what's wrong. That's the whole point. If you see Nest application successfully started in the console, you nailed it. At this point, you’ve built a configuration layer that is: Centralized (one module) Type-safe (structured config objects) Validated (Joi schema) Ready for the next steps (Drizzle + Supabase integration) In the next post, we'll use this module to initialize Drizzle with the Supabase Postgres connection string and start defining our schema. But to be ready, we'll need the database config—so that's your homework. See you in the next one! 💡 Next post: Connecting Supabase Postgres to NestJS using Drizzle and our Config Module. 🔗 Code: https://github.com/RubenOAlvarado/finance-api/tree/v0.2.0 Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
nest g mo config Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
nest g mo config CODE_BLOCK:
nest g mo config CODE_BLOCK:
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config'; @Global()
@Module({ imports: [ NestConfigModule.forRoot({ isGlobal: true, cache: true, envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], }), ], exports: [NestConfigModule],
})
export class ConfigModule {} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config'; @Global()
@Module({ imports: [ NestConfigModule.forRoot({ isGlobal: true, cache: true, envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], }), ], exports: [NestConfigModule],
})
export class ConfigModule {} CODE_BLOCK:
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config'; @Global()
@Module({ imports: [ NestConfigModule.forRoot({ isGlobal: true, cache: true, envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], }), ], exports: [NestConfigModule],
})
export class ConfigModule {} COMMAND_BLOCK:
// config/configurations/app.config.ts
import { registerAs } from '@nestjs/config'; export default registerAs( 'app', (): AppConfig => ({ port: parseInt(process.env.PORT || '3000', 10), nodeEnv: (process.env.NODE_ENV || 'development') as AppConfig['nodeEnv'], }),
); // config/types/app-config.types.ts
export type AppConfig = { port: number; nodeEnv: 'development' | 'production' | 'test' | 'staging';
}; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// config/configurations/app.config.ts
import { registerAs } from '@nestjs/config'; export default registerAs( 'app', (): AppConfig => ({ port: parseInt(process.env.PORT || '3000', 10), nodeEnv: (process.env.NODE_ENV || 'development') as AppConfig['nodeEnv'], }),
); // config/types/app-config.types.ts
export type AppConfig = { port: number; nodeEnv: 'development' | 'production' | 'test' | 'staging';
}; COMMAND_BLOCK:
// config/configurations/app.config.ts
import { registerAs } from '@nestjs/config'; export default registerAs( 'app', (): AppConfig => ({ port: parseInt(process.env.PORT || '3000', 10), nodeEnv: (process.env.NODE_ENV || 'development') as AppConfig['nodeEnv'], }),
); // config/types/app-config.types.ts
export type AppConfig = { port: number; nodeEnv: 'development' | 'production' | 'test' | 'staging';
}; CODE_BLOCK:
pnpm i joi Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// config/validations/env.validation.ts
import * as Joi from 'joi'; export const envValidationSchema = Joi.object({ PORT: Joi.number().default(3000), NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'staging') .default('development'),
}); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// config/validations/env.validation.ts
import * as Joi from 'joi'; export const envValidationSchema = Joi.object({ PORT: Joi.number().default(3000), NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'staging') .default('development'),
}); CODE_BLOCK:
// config/validations/env.validation.ts
import * as Joi from 'joi'; export const envValidationSchema = Joi.object({ PORT: Joi.number().default(3000), NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'staging') .default('development'),
}); CODE_BLOCK:
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config'; import appConfig from './configurations/app.config';
import { envValidationSchema } from './validations/env.validation'; @Global()
@Module({ imports: [ NestConfigModule.forRoot({ isGlobal: true, cache: true, envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], load: [appConfig], validationSchema: envValidationSchema, }), ], exports: [NestConfigModule],
})
export class ConfigModule {} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config'; import appConfig from './configurations/app.config';
import { envValidationSchema } from './validations/env.validation'; @Global()
@Module({ imports: [ NestConfigModule.forRoot({ isGlobal: true, cache: true, envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], load: [appConfig], validationSchema: envValidationSchema, }), ], exports: [NestConfigModule],
})
export class ConfigModule {} CODE_BLOCK:
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config'; import appConfig from './configurations/app.config';
import { envValidationSchema } from './validations/env.validation'; @Global()
@Module({ imports: [ NestConfigModule.forRoot({ isGlobal: true, cache: true, envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], load: [appConfig], validationSchema: envValidationSchema, }), ], exports: [NestConfigModule],
})
export class ConfigModule {} - One typo silently breaks your connection.
- One missing variable makes the app crash at runtime.
- You end up debugging configuration instead of shipping. - One place to load and validate environment variables
- Clear namespaces like app, and db (for now)
- Type inference so configuration access is safe and discoverable
- Fail fast validations (before the app starts) - Drizzle needs a valid Postgres connection string
- Local and production environments must behave predictably - Global config (no need to import it everywhere)
- Environment-specific .env loading
- Caching for performance - db.connectionString - Centralized (one module)
- Type-safe (structured config objects)
- Validated (Joi schema)
- Ready for the next steps (Drizzle + Supabase integration)
how-totutorialguidedev.toainodedatabasegitgithub