# Developing a Tailored Config Module for NestJS Applications

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.

## Why a custom Config Module?

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
    

## Creating the module

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):

```bash
nest g mo config
```

Now set up the module using Nest’s `ConfigModule`, but keep it wrapped behind your own module.

Here’s the baseline:

```tsx
// 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 {}
```

This already gives us:

* Global config (no need to import it everywhere)
    
* Environment-specific `.env` loading
    
* Caching for performance
    

## Defining configuration namespaces (app, db)

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`.

```tsx
// 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';
};
```

For this series, the important part is that by the time we connect Drizzle, we can read something like:

* `db.connectionString`
    

## Validating environment variables with Joi

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.

Install Joi:

```bash
pnpm i joi
```

Create a validation schema:

```tsx
// 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'),
});
```

Now wire everything together:

```tsx
// 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 {}
```

## Testing it quickly

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.

## Wrapping Up

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!

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Next post</strong>: Connecting Supabase Postgres to NestJS using Drizzle and our Config Module.</div>
</div>

---

**🔗 Code**: [https://github.com/RubenOAlvarado/finance-api/tree/v0.2.0](https://github.com/RubenOAlvarado/finance-api/tree/v0.2.0)
