Validating Environment Variables

Fail Fast on Bad Config

A missing JWT_SECRET should stop the app at startup, not crash the first login in production. ConfigModule.forRoot accepts a validate function. A common approach uses class-validator:

src/config/env.validation.ts

import { plainToInstance } from 'class-transformer';
import { IsEnum, IsInt, IsString, Max, Min, MinLength, validateSync } from 'class-validator';

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

class EnvironmentVariables {
  @IsEnum(Environment) NODE_ENV: Environment;
  @IsInt() @Min(1) @Max(65535) PORT: number;
  @IsString() DATABASE_URL: string;
  @IsString() @MinLength(32) JWT_SECRET: string;
}

export function validate(config: Record<string, unknown>) {
  const env = plainToInstance(EnvironmentVariables, config, { enableImplicitConversion: true });
  const errors = validateSync(env, { skipMissingProperties: false });
  if (errors.length > 0) throw new Error(errors.toString());
  return env;
}

// app.module.ts:  ConfigModule.forRoot({ isGlobal: true, validate })

Example

npm run start:dev

Example output

Error: An instance of EnvironmentVariables has failed the validation:
 - property JWT_SECRET has failed the following constraints: minLength

The Same Idea, Runnable

Example

A small typed env loader: convert strings, apply defaults and collect every problem before failing.

type EnvSpec = {
  [key: string]: { type: 'string' | 'number' | 'enum'; values?: string[]; min?: number; default?: string };
};

function loadEnv(raw: Record<string, string | undefined>, spec: EnvSpec) {
  const errors: string[] = [];
  const out: Record<string, string | number> = {};
  for (const [key, rule] of Object.entries(spec)) {
    const value = raw[key] ?? rule.default;
    if (value === undefined) { errors.push(key + ' is required'); continue; }
    if (rule.type === 'number') {
      const n = Number(value);
      if (Number.isNaN(n)) errors.push(key + ' must be a number');
      else out[key] = n;
    } else if (rule.type === 'enum' && !rule.values!.includes(value)) {
      errors.push(key + ' must be one of ' + rule.values!.join('|'));
    } else if (rule.min && value.length < rule.min) {
      errors.push(key + ' must be at least ' + rule.min + ' characters');
    } else out[key] = value;
  }
  if (errors.length) throw new Error('Invalid environment: ' + errors.join('; '));
  return out;
}

const spec: EnvSpec = {
  NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'], default: 'development' },
  PORT: { type: 'number', default: '3000' },
  JWT_SECRET: { type: 'string', min: 16 },
};

console.log(loadEnv({ JWT_SECRET: 'a-very-long-secret-value' }, spec));
try {
  loadEnv({ NODE_ENV: 'prod', PORT: 'abc', JWT_SECRET: 'short' }, spec);
} catch (e) {
  console.log((e as Error).message);
}

Output

{
  NODE_ENV: 'development',
  PORT: 3000,
  JWT_SECRET: 'a-very-long-secret-value'
}
Invalid environment: NODE_ENV must be one of development|production|test; PORT must be a number; JWT_SECRET must be at least 16 characters