@nestjs/config reads .env files and process.env, exposes the result through an injectable ConfigService, and — the part that matters — runs a validation function over the whole environment at boot. Secrets and Audits's rule stands: a server that refuses to boot with a bad secret is safer than one that boots and signs tokens with undefined.
export class Env {
@IsString() @Length(32, 200) ACCESS_SECRET!: string; // no default, on purpose
@IsInt() @Min(1) @Max(65535) PORT: number = 4310;
}
export function envSchema(raw: Record<string, unknown>) {
const env = plainToInstance(Env, raw, { enableImplicitConversion: true });
const errs = validateSync(env, { skipMissingProperties: false });
if (errs.length) throw new Error('configuration error:\n' +
errs.map((e) => ` ${e.property}: ${Object.values(e.constraints ?? {})[0]}`).join('\n'));
return env; // the returned object is what ConfigService serves
} // ConfigModule.forRoot({ isGlobal: true, cache: true, validate: envSchema })enableImplicitConversion matters here for the same reason as in the validation pipe: every value in process.env is a string, so PORT arrives as "4310" and @IsInt() would reject it. Read values with getOrThrow wherever a missing value is a bug: config.getOrThrow<string>('ACCESS_SECRET') fails loudly at startup, while config.get('ACCES_SECRET') — one letter wrong — silently returns undefined and produces tokens nobody can verify.