Skip to content
NestJS ns config 4 min read

Config Validation

A missing DATABASE_URL or a typo like PORT=threethousand should crash your application immediately at boot — not hours later when the first request hits a broken connection. @nestjs/config supports validating the entire environment before the application finishes bootstrapping, so invalid configuration becomes a loud, descriptive startup error instead of a silent runtime bug. You can validate with a declarative Joi schema or with a custom function backed by class-validator. This page covers both approaches and the allowUnknown / abortEarly options that control how strict the check is.

Why fail fast

Configuration errors that surface late are expensive to debug. By validating at startup you guarantee that, once the app is listening, every variable your code reads exists and has the right shape. Validation runs once during ConfigModule.forRoot(), throws if anything is wrong, and prevents the process from ever reaching a half-configured state. This is the single most effective way to make environment configuration safe.

Validation only runs against variables that pass through ConfigModule. Reading process.env directly bypasses it entirely — always go through ConfigService.

Validating with Joi

Joi is the most common choice because the schema is concise and self-documenting. Install it as a runtime dependency, then pass a validationSchema to forRoot.

npm install joi

Define the schema with Joi.object(), listing every variable your app expects. .required() makes a key mandatory, .default() supplies a fallback, and .valid() restricts a key to an enum of allowed values.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test')
          .default('development'),
        PORT: Joi.number().port().default(3000),
        DATABASE_URL: Joi.string().uri().required(),
        JWT_SECRET: Joi.string().min(16).required(),
        FEATURE_FLAGS: Joi.string().default(''),
      }),
      validationOptions: {
        allowUnknown: true,
        abortEarly: false,
      },
    }),
  ],
})
export class AppModule {}

If JWT_SECRET is absent, Nest never finishes bootstrapping:

Output:

Error: Config validation error: "JWT_SECRET" is required. "PORT" must be a valid port
    at ConfigModule.forRoot ...

Because Joi coerces types, PORT is now a real number and NODE_ENV is guaranteed to be one of the three allowed strings — so config.get<number>('PORT') actually returns a number, not a string.

Validation options

The validationOptions object is passed straight to Joi. Two flags matter most:

OptionDefaultEffect
allowUnknowntrueWhen true, variables not listed in the schema are permitted. Set false to reject any unexpected key.
abortEarlyfalse (set by Nest)When false, Joi reports all errors at once instead of stopping at the first.
converttrueCoerces values to the schema type (e.g. the string "3000" into the number 3000).
stripUnknownfalseRemoves unknown keys from the validated result rather than passing them through.

Keeping allowUnknown: true is sensible because the OS injects dozens of unrelated variables (PATH, HOME, PWD) that you do not want to enumerate. Setting abortEarly: false means a developer with three misconfigured variables sees all three problems in one run instead of fixing them one at a time.

Validating with a custom function

If you prefer not to add Joi, supply a validate function instead. This pairs naturally with class-validator and class-transformer, giving you a strongly typed, class-based schema that doubles as a config interface.

npm install class-validator class-transformer
// src/env.validation.ts
import { plainToInstance } from 'class-transformer';
import { IsEnum, IsNumber, IsString, MinLength, validateSync } from 'class-validator';

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

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;

  @IsString()
  @MinLength(16)
  JWT_SECRET: string;
}

export function validate(config: Record<string, unknown>) {
  const validated = plainToInstance(EnvironmentVariables, config, {
    enableImplicitConversion: true,
  });

  const errors = validateSync(validated, { skipMissingProperties: false });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validated;
}

Register it the same way — Nest calls your function with the full environment object and uses whatever you return as the validated config.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { validate } from './env.validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validate,
    }),
  ],
})
export class AppModule {}

The validate function must throw to signal failure and must return the validated object. With enableImplicitConversion: true, class-transformer coerces the string PORT into a number so the @IsNumber() check passes.

Joi vs custom validate

AspectJoi validationSchemaCustom validate
Extra dependencyjoiclass-validator, class-transformer
StyleDeclarative, fluent schemaClass with decorators
Type coercionBuilt in via convertVia enableImplicitConversion
Reuse as a typed interfaceNoYes — the class is your config shape

Best Practices

  • Validate at startup so misconfiguration fails fast and visibly instead of corrupting runtime behavior.
  • Keep abortEarly: false so every invalid variable is reported in a single run.
  • Leave allowUnknown: true to tolerate the many unrelated OS variables present in every environment.
  • Mark mandatory secrets .required() (Joi) or non-optional (class-validator) — never let a missing secret default to something insecure.
  • Lean on type coercion so downstream code receives real numbers and booleans, not strings.
  • Pick one approach per project: Joi for terse schemas, class-validator when you want the schema to double as a typed config class.
Last updated June 14, 2026
Was this helpful?