Skip to main content

Advanced configuration

This guide covers advanced configuration patterns including default values, optional fields, and mixed schema definitions.

Default values

Provide default values for variables that aren’t critical to set:
import { validateEnv } from 'env-core';

const env = validateEnv({
  DEBUG: { 
    type: Boolean,
    default: false,
  },
  HOST: {
    type: String,
    default: '0.0.0.0',
  },
  PORT: {
    type: Number,
    default: 3000,
  },
});
Now these variables are optional in your .env file:
.env
# PORT not specified - will use default 3000
# DEBUG not specified - will use default false
HOST=localhost
When a variable has a default value, it automatically becomes optional. You don’t need to set required: false.

Optional fields

Make fields optional without providing a default value:
const env = validateEnv({
  OPTIONAL_VAR: {
    type: String,
    required: false,
  },
  ANOTHER_OPTIONAL: {
    type: Number,
    required: false,
  },
});
If these variables aren’t set, they’ll be undefined:
if (env.OPTIONAL_VAR !== undefined) {
  console.log(`Optional var: ${env.OPTIONAL_VAR}`);
} else {
  console.log('Optional var not set');
}
Optional fields without defaults will be undefined if not set. Make sure to handle this in your code.

Mixed schema approach

Combine simple types (required) with configuration objects (optional with defaults) for maximum clarity:
const env = validateEnv({
  // Required variables - simple syntax
  PORT: Number,
  NODE_ENV: String,
  DATABASE_URL: String,
  
  // Optional with defaults - configuration syntax
  DEBUG: {
    type: Boolean,
    default: false,
  },
  HOST: {
    type: String,
    default: '0.0.0.0',
  },
  LOG_LEVEL: {
    type: String,
    default: 'info',
  },
});
.env
# Required - must be set
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://localhost/myapp

# Optional - can be omitted
# DEBUG defaults to false
# HOST defaults to 0.0.0.0
LOG_LEVEL=debug
This is the recommended approach - it’s immediately clear which variables are required and which have sensible defaults.

Environment-specific defaults

Use different defaults based on the current environment:
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';

const env = validateEnv({
  NODE_ENV: String,
  
  // Development-friendly defaults
  DEBUG: {
    type: Boolean,
    default: isDevelopment,  // true in dev, false otherwise
  },
  LOG_LEVEL: {
    type: String,
    default: isDevelopment ? 'debug' : 'info',
  },
  
  // Different port defaults
  PORT: {
    type: Number,
    default: isDevelopment ? 3000 : 8080,
  },
});

Configuration with all options

Here’s an example using all configuration options:
const env = validateEnv({
  // Simple required
  API_KEY: String,
  
  // Required with explicit flag
  DATABASE_URL: {
    type: String,
    required: true,
  },
  
  // Optional with default
  PORT: {
    type: Number,
    default: 3000,
  },
  
  // Optional without default
  OPTIONAL_FEATURE: {
    type: Boolean,
    required: false,
  },
  
  // Required with default (unusual but valid)
  // Will use default only if not set, but won't fail if missing
  CACHE_TTL: {
    type: Number,
    default: 3600,
    required: false,
  },
});

Practical examples

Web server configuration

const env = validateEnv({
  // Critical settings - required
  PORT: Number,
  NODE_ENV: String,
  
  // Server settings - sensible defaults
  HOST: { type: String, default: '0.0.0.0' },
  TIMEOUT_MS: { type: Number, default: 30000 },
  MAX_CONNECTIONS: { type: Number, default: 100 },
  
  // Feature flags - default to false
  ENABLE_COMPRESSION: { type: Boolean, default: false },
  ENABLE_RATE_LIMITING: { type: Boolean, default: false },
  
  // Optional features
  REDIS_URL: { type: String, required: false },
  SENTRY_DSN: { type: String, required: false },
});

Database configuration

const env = validateEnv({
  // Required - cannot run without database
  DATABASE_URL: String,
  
  // Connection pool - reasonable defaults
  DB_POOL_MIN: { type: Number, default: 2 },
  DB_POOL_MAX: { type: Number, default: 10 },
  DB_TIMEOUT_MS: { type: Number, default: 5000 },
  
  // SSL - optional, often needed in production
  DB_SSL: { type: Boolean, default: false },
  DB_SSL_REJECT_UNAUTHORIZED: { type: Boolean, default: true },
  
  // Optional read replica
  DATABASE_READ_REPLICA_URL: { type: String, required: false },
});

API client configuration

const env = validateEnv({
  // Required credentials
  API_KEY: String,
  API_URL: String,
  
  // Request settings
  API_TIMEOUT_MS: { type: Number, default: 5000 },
  API_RETRY_ATTEMPTS: { type: Number, default: 3 },
  API_RETRY_DELAY_MS: { type: Number, default: 1000 },
  
  // Optional settings
  API_DEBUG: { type: Boolean, default: false },
  API_CUSTOM_HEADER: { type: String, required: false },
});

Handling undefined optional values

When using optional fields without defaults, handle undefined values:
const env = validateEnv({
  REDIS_URL: { type: String, required: false },
  CACHE_ENABLED: { type: Boolean, required: false },
});

// Option 1: Check before using
if (env.REDIS_URL !== undefined) {
  connectToRedis(env.REDIS_URL);
}

// Option 2: Provide fallback with ??
const redisUrl = env.REDIS_URL ?? 'redis://localhost:6379';

// Option 3: Conditional logic
const cacheEnabled = env.CACHE_ENABLED ?? false;
if (cacheEnabled) {
  enableCaching();
}

TypeScript with optional values

TypeScript correctly infers optional types:
const env = validateEnv({
  REQUIRED: String,
  WITH_DEFAULT: { type: Number, default: 42 },
  OPTIONAL: { type: Boolean, required: false },
});

// Types:
env.REQUIRED     // string
env.WITH_DEFAULT // number
env.OPTIONAL     // boolean | undefined

// Handle optional values
if (env.OPTIONAL !== undefined) {
  const value: boolean = env.OPTIONAL;  // ✅ TypeScript knows it's defined here
}

Best practices

Provide sensible defaults for non-critical configuration to make local development easier:
{
  PORT: { type: Number, default: 3000 },
  DEBUG: { type: Boolean, default: true },
}
Don’t provide defaults for critical settings like database URLs or API keys:
{
  DATABASE_URL: String,  // Required - no default
  API_KEY: String,       // Required - no default
}
Add comments explaining default values:
{
  // Default: 10 connections (sufficient for most apps)
  DB_POOL_SIZE: { type: Number, default: 10 },
  
  // Default: false (enable in production for performance)
  ENABLE_CACHING: { type: Boolean, default: false },
}
Different defaults for development vs production:
const isDev = process.env.NODE_ENV === 'development';

{
  DEBUG: { type: Boolean, default: isDev },
  LOG_LEVEL: { type: String, default: isDev ? 'debug' : 'info' },
}

Next steps

Custom env files

Learn about using custom .env files

Schema definition

Deep dive into schema definition patterns

Build docs developers (and LLMs) love