Config Package
The @hazeljs/config package provides a robust configuration management system for your HazelJS applications. It supports loading from environment variables, .env files, custom loaders, and schema validation.
Purpose
Managing application configuration across different environments (development, staging, production) is a common challenge. You need to load values from multiple sources, validate required settings, provide defaults, and ensure type safety. The @hazeljs/config package solves these problems by providing:
- Multi-Source Loading: Load from environment variables,
.envfiles, and custom sources - Schema Validation: Validate configuration at startup to catch errors early
- Type Safety: TypeScript-first design with proper type inference
- Nested Configuration: Support for nested config objects with dot notation
- Environment-Specific: Easy management of different configs per environment
Architecture
The package uses a layered loading approach that prioritizes sources and validates configuration:
Key Features
- Layered Loading: Loads from multiple sources with priority ordering
- Validation: Schema-based validation ensures required config is present
- Type Safety: Full TypeScript support with type inference
- Global Access: Optional global configuration for easy access
Advantages
1. Early Error Detection
Schema validation catches missing or invalid configuration at startup, preventing runtime errors.
2. Developer Experience
Simple API with config.get() and config.getOrThrow() makes accessing configuration intuitive.
3. Type Safety
TypeScript support ensures you get autocomplete and type checking for your configuration values.
4. Flexibility
Load from environment variables, files, databases, or custom sources. Mix and match as needed.
5. Environment Management
Easy to manage different configurations for development, staging, and production environments.
6. Nested Support
Access nested configuration with dot notation: config.get('database.host').
Installation
npm install @hazeljs/config
Quick Start
Basic Setup
import { HazelModule } from '@hazeljs/core';
import { ConfigModule } from '@hazeljs/config';
@HazelModule({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
Configuration Service
The ConfigService is the core service for accessing configuration values throughout your application. It provides a type-safe, flexible API for reading configuration from multiple sources.
Understanding ConfigService
Purpose: Provides a unified interface for accessing configuration values from environment variables, .env files, and custom sources.
How it works:
- Multi-Source Loading: Loads from
.envfiles,process.env, and custom loaders - Priority Ordering: Later sources override earlier ones
- Type Safety: TypeScript generics ensure type-safe access
- Nested Access: Supports dot notation for nested configuration
- Validation: Optional schema validation at startup
Basic Usage
Inject ConfigService to access configuration values:
import { Injectable } from '@hazeljs/core';
import { ConfigService } from '@hazeljs/config';
@Injectable()
export class AppService {
constructor(private readonly config: ConfigService) {}
getApiUrl() {
// config.get() with default value
// Returns the configured value or the default if not found
return this.config.get<string>('API_URL', 'http://localhost:3000');
// Type parameter <string> ensures type safety
// Default value provides fallback
}
getDatabaseUrl() {
// config.getOrThrow() requires the value to exist
// Throws error if configuration is missing
return this.config.getOrThrow<string>('DATABASE_URL');
// Use this for required configuration
// Fails fast at startup if missing
}
checkFeatureFlag() {
// Check if configuration exists
if (this.config.has('FEATURE_NEW_UI')) {
const enabled = this.config.get<boolean>('FEATURE_NEW_UI', false);
return enabled;
}
return false;
}
}
Configuration Methods Explained
1. get<T>(key: string, defaultValue?: T): T | undefined
Retrieves a configuration value with optional default:
// With default value
const port = this.config.get<number>('PORT', 3000);
// Returns configured PORT or 3000 if not found
// Type is inferred as number
// Without default (returns undefined if not found)
const apiKey = this.config.get<string>('API_KEY');
// Returns string | undefined
// Use when configuration is optional
2. getOrThrow<T>(key: string): T
Retrieves a required configuration value, throwing if missing:
const dbUrl = this.config.getOrThrow<string>('DATABASE_URL');
// Returns string (never undefined)
// Throws Error if DATABASE_URL is not configured
// Use for required configuration
// Fails fast at runtime if missing
3. has(key: string): boolean
Checks if a configuration key exists:
if (this.config.has('FEATURE_FLAG')) {
// Configuration exists, safe to access
const flag = this.config.get<boolean>('FEATURE_FLAG');
}
4. set(key: string, value: unknown): void
Dynamically sets a configuration value:
// Set configuration at runtime
this.config.set('CACHE_TTL', 3600);
this.config.set('feature.enabled', true);
// Useful for:
// - Runtime configuration updates
// - Feature flags
// - Dynamic settings
5. getAll(): Record<string, unknown>
Retrieves all configuration as an object:
const allConfig = this.config.getAll();
// Returns: { DATABASE_URL: '...', PORT: 3000, ... }
// Useful for debugging or configuration inspection
Environment Files
Load configuration from .env files:
ConfigModule.forRoot({
envFilePath: '.env',
// or multiple files
envFilePath: ['.env', '.env.local'],
})
Ignore Environment Files
ConfigModule.forRoot({
ignoreEnvFile: true, // Don't load .env files
ignoreEnvVars: false, // Still load from process.env
})
Configuration Methods
Get Configuration Value
// Get with optional default
const port = this.config.get<number>('PORT', 3000);
// Get or throw if missing
const apiKey = this.config.getOrThrow<string>('API_KEY');
// Check if key exists
if (this.config.has('FEATURE_FLAG')) {
const flag = this.config.get<boolean>('FEATURE_FLAG');
}
Nested Configuration
Access nested configuration using dot notation. This allows you to organize related configuration values logically.
How it works:
- Dot notation (
database.host) accesses nested properties - Framework traverses the configuration object
- Returns
undefinedif any part of the path doesn't exist
Example with Detailed Explanation:
// Environment variables or .env file:
// DATABASE_HOST=localhost
// DATABASE_PORT=5432
// DATABASE_NAME=mydb
// DATABASE_SSL=true
// Or structured in .env:
// DATABASE__HOST=localhost (double underscore for nesting)
// DATABASE__PORT=5432
// DATABASE__NAME=mydb
// Access nested configuration
const host = this.config.get<string>('database.host');
// Traverses: config.database.host
// Returns 'localhost' or undefined
const port = this.config.get<number>('database.port');
// Returns 5432 (as number) or undefined
const name = this.config.get<string>('database.name');
// Returns 'mydb' or undefined
// Deep nesting
const ssl = this.config.get<boolean>('database.ssl.enabled');
// Traverses: config.database.ssl.enabled
Nested Configuration Structure:
You can structure configuration hierarchically:
// .env file
DATABASE__HOST=localhost
DATABASE__PORT=5432
DATABASE__CREDENTIALS__USERNAME=admin
DATABASE__CREDENTIALS__PASSWORD=secret
// Access with dot notation
const host = this.config.get<string>('database.host');
const username = this.config.get<string>('database.credentials.username');
const password = this.config.get<string>('database.credentials.password');
Benefits of Nested Configuration:
- Organization: Group related settings together
- Namespace: Avoid naming conflicts
- Clarity: Makes configuration structure clear
- Type Safety: Can define interfaces for nested config
Set Configuration
Dynamically set configuration values:
this.config.set('CACHE_TTL', 3600);
this.config.set('feature.enabled', true);
Get All Configuration
const allConfig = this.config.getAll();
console.log(allConfig);
Schema Validation
Validate configuration against a schema:
import { ConfigModule } from '@hazeljs/config';
const validationSchema = {
validate(config: Record<string, unknown>) {
const errors: string[] = [];
if (!config.DATABASE_URL) {
errors.push('DATABASE_URL is required');
}
if (!config.JWT_SECRET) {
errors.push('JWT_SECRET is required');
}
if (errors.length > 0) {
return {
error: new Error(errors.join(', ')),
value: config,
};
}
return { value: config };
},
};
ConfigModule.forRoot({
validationSchema,
validationOptions: {
abortEarly: false, // Collect all errors
allowUnknown: true, // Allow extra keys
},
})
Custom Loaders
Load configuration from custom sources:
ConfigModule.forRoot({
load: [
() => ({
// Load from database
DATABASE_URL: 'postgresql://localhost:5432/mydb',
}),
() => ({
// Load from external API
EXTERNAL_API_KEY: fetchApiKey(),
}),
() => ({
// Load from file
...require('./config.json'),
}),
],
})
Global Configuration
Make configuration available globally:
ConfigModule.forRoot({
isGlobal: true, // Available in all modules without importing
})
Complete Example
import { HazelModule } from '@hazeljs/core';
import { ConfigModule, ConfigService } from '@hazeljs/config';
import { Injectable } from '@hazeljs/core';
// Module setup
@HazelModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env', '.env.local'],
validationSchema: {
validate(config) {
const required = ['DATABASE_URL', 'JWT_SECRET'];
const missing = required.filter(key => !config[key]);
if (missing.length > 0) {
return {
error: new Error(`Missing required config: ${missing.join(', ')}`),
value: config,
};
}
return { value: config };
},
},
isGlobal: true,
}),
],
})
export class AppModule {}
// Service usage
@Injectable()
export class DatabaseService {
constructor(private readonly config: ConfigService) {}
getConnectionString() {
return this.config.getOrThrow<string>('DATABASE_URL');
}
getConfig() {
return {
host: this.config.get<string>('database.host', 'localhost'),
port: this.config.get<number>('database.port', 5432),
ssl: this.config.get<boolean>('database.ssl', false),
};
}
}
Environment Variables
Create a .env file:
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
DATABASE_HOST=localhost
DATABASE_PORT=5432
# JWT
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d
# API
API_URL=https://api.example.com
API_KEY=your-api-key
# Feature Flags
FEATURE_NEW_UI=true
FEATURE_BETA=false
Best Practices
-
Use validation: Always validate required configuration at startup.
-
Environment-specific files: Use
.env.localfor local development overrides. -
Type safety: Use TypeScript types when getting configuration values.
-
Sensitive data: Never commit
.envfiles with secrets. Use environment variables in production. -
Default values: Provide sensible defaults for optional configuration.
-
Nested config: Use dot notation for related configuration values.