# Configuration and Environment

> Source: https://learn-nestjs.com/configuration/
> Part of Learn NestJS, free to read.

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

```typescript src/app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,                      // available everywhere, no re-import
      envFilePath: ['.env.local', '.env'], // first match wins
      cache: true,
    }),
  ],
})
export class AppModule {}
```

```typescript
@Injectable()
export class MailService {
  constructor(private readonly config: ConfigService) {}

  send() {
    const key = this.config.get<string>('MAIL_API_KEY');
  }
}
```

That works, and it has two problems worth fixing straight away: **nothing checks the variable exists**, and `get<string>()` is an assertion rather than a guarantee — the type parameter is you telling TypeScript what to believe.

## Validate at boot

The most valuable change you can make here.

```typescript src/config/env.schema.ts
import { z } from 'zod';

export const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().positive().default(3000),

  DATABASE_URL: z.url(),
  DATABASE_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),

  JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
  JWT_EXPIRES_IN: z.string().default('15m'),

  REDIS_URL: z.url().optional(),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

export type Env = z.infer<typeof EnvSchema>;
```

```typescript
ConfigModule.forRoot({
  isGlobal: true,
  validate: (raw) => EnvSchema.parse(raw),   // throws at startup, not at 2am
})
```

Now a missing `DATABASE_URL` or a 12-character `JWT_SECRET` stops the application from starting, with a message naming the variable. Compare that with the alternative: the app boots, serves traffic, and fails on the first request that touches the database.

Note `z.coerce.number()` — every environment variable is a string, so numeric settings need coercion. A `PORT` of `"3000"` passed where a number is expected is a classic source of confusing failures.

:::verdict Fail fast, loudly, at boot
This is the whole point. A configuration error should be impossible to deploy, not a runtime surprise. It also means your container orchestrator sees a failed start and stops the rollout, rather than routing traffic to a broken instance.
:::

## Typed access

`config.get<string>('X')` is an unchecked assertion. Namespaced configuration gives you real types:

```typescript src/config/database.config.ts
export default registerAs('database', () => ({
  url: process.env.DATABASE_URL!,
  poolSize: Number(process.env.DATABASE_POOL_SIZE ?? 10),
}));

export type DatabaseConfig = ConfigType<typeof databaseConfig>;
```

```typescript
ConfigModule.forRoot({
  isGlobal: true,
  load: [databaseConfig, authConfig],
  validate: (raw) => EnvSchema.parse(raw),
})
```

```typescript
@Injectable()
export class Repo {
  constructor(
    @Inject(databaseConfig.KEY)
    private readonly config: DatabaseConfig,   // fully typed, no generics
  ) {}

  connect() {
    return createPool(this.config.url, { max: this.config.poolSize });
  }
}
```

The service now depends on a small typed object rather than on `ConfigService` and a set of string keys. That is easier to test — you pass a plain object — and a renamed variable becomes a compile error rather than an `undefined`.

## Async module configuration

Modules that need config at construction time use the `forRootAsync` convention:

```typescript
JwtModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    secret: config.getOrThrow<string>('JWT_SECRET'),
    signOptions: { expiresIn: config.getOrThrow<string>('JWT_EXPIRES_IN') },
  }),
}),
```

`getOrThrow` rather than `get` — it fails loudly instead of silently passing `undefined` as your signing secret, which would produce tokens anyone can forge.

## Per-environment files

```text
.env                 committed — safe defaults only, no secrets
.env.local           gitignored — your machine
.env.test            committed — test values, no real credentials
.env.production      NEVER committed
```

```bash .gitignore
.env.local
.env.*.local
.env.production
```

Commit a `.env.example` listing every variable with a placeholder. It is the only documentation of your configuration surface that stays current, because a missing entry breaks someone's setup immediately.

```bash .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp_dev
JWT_SECRET=generate-with-openssl-rand-base64-32
```

## In production, prefer real secret management

`.env` files are a development convenience. In production, inject configuration through your platform — container environment variables, a secrets manager, or your orchestrator's secret objects. A `.env` file on a production host is a file that can be read, copied and backed up.

Whatever the source, the schema validation above still applies: the application checks what it received and refuses to start if it is wrong.

## Exercise

```typescript
// 1. Write a schema requiring: NODE_ENV, PORT (number, default 3000),
//    DATABASE_URL (a URL), STRIPE_SECRET_KEY (starts with "sk_"),
//    and an optional SENTRY_DSN.
// 2. Wire it into ConfigModule so a bad value stops startup.
// 3. Create a namespaced `stripe` config and inject it, typed,
//    into a PaymentsService — with no ConfigService and no generics.
```

## Common questions

### Should I validate configuration if I have a `.env.example`?

Yes. The example file documents intent; validation enforces it. They fail differently — the example does not stop someone deploying with `JWT_SECRET=changeme`, and the schema does.

### `ConfigService.get` or namespaced config?

Namespaced, for anything beyond a couple of values. It gives you real types instead of an assertion, makes services testable with a plain object, and turns a renamed variable into a compile error rather than an `undefined` at runtime.

### Is `isGlobal: true` a good idea?

For configuration, yes — it is genuinely needed everywhere and re-importing `ConfigModule` in twenty modules is noise. That is the narrow case where `@Global()` is justified; resist it for domain modules, where the explicit import edge is the thing that keeps the dependency graph readable.
