@neoma/config
v0.3.0
Published
Simple, type-safe environment configuration for NestJS applications
Downloads
112
Maintainers
Readme
@neoma/config
Simple, type-safe environment configuration for NestJS applications.
The Problem
NestJS's built-in ConfigService adds unnecessary complexity:
- Needs schema validation setup for type safety
- Still requires repetitive
get()calls with magic strings
Even worse than raw process.env calls, you end up maintaining configuration boilerplate that grows with every new environment variable.
The Solution
@neoma/config provides a clean, type-safe way to access environment variables through dependency injection:
// Before: Repetitive, untyped, error-prone
class DatabaseService {
connect() {
const host = process.env.DATABASE_HOST
const port = process.env.DATABASE_PORT
const user = process.env.DATABASE_USER
// No type checking, no autocomplete
}
}
// After: Clean, typed, injected
class DatabaseService {
constructor(
@InjectConfig()
private config: TypedConfig<{
databaseHost: string
databasePort: string
databaseUser: string
}>,
) {}
connect() {
const { databaseHost, databasePort, databaseUser } = this.config
// Full type safety and autocomplete!
}
}Installation
npm install @neoma/configBasic Usage
1. Import the ConfigModule
import { Module } from "@nestjs/common"
import { ConfigModule } from "@neoma/config"
@Module({
imports: [ConfigModule],
})
export class AppModule {}2. Load environment variables (optional)
import { Module } from '@nestjs/common'
import { ConfigModule } from '@neoma/config'
@Module({
imports: [
// Load .env files automatically
ConfigModule.forRoot({ loadEnv: true })
],
})
export class AppModule {}This automatically loads environment variables from:
.env.{NODE_ENV}.local(highest priority).env.local.env.{NODE_ENV}.env(lowest priority)
3. Inject and use configuration
import { Injectable } from "@nestjs/common"
import { InjectConfig, TypedConfig } from "@neoma/config"
@Injectable()
export class AppService {
constructor(
@InjectConfig()
private config: TypedConfig<{
apiKey: string
apiUrl: string
debugMode: string
}>,
) {}
makeRequest() {
// Access environment variables with type safety
const url = this.config.apiUrl // reads from API_URL
const key = this.config.apiKey // reads from API_KEY
const debug = this.config.debugMode // reads from DEBUG_MODE
}
}Naming Convention Flexibility
The package automatically converts between camelCase/PascalCase and SCREAMING_SNAKE_CASE, supporting multiple coding styles:
// All of these work with DATABASE_URL environment variable:
config.databaseUrl // camelCase (JavaScript convention)
config.databaseURL // Mixed case (common for acronyms)
config.database_url // Snake case (if you prefer)
// Complex examples:
config.apiKey // API_KEY
config.apiURL // API_URL
config.awsS3Bucket // AWS_S3_BUCKET
config.awsS3BucketName // AWS_S3_BUCKET_NAMEType Safety
Define your configuration interface for full TypeScript support:
interface AppConfig {
// Required configuration
databaseUrl: string
redisHost: string
jwtSecret: string
// Optional configuration
port?: string
logLevel?: string
}
@Injectable()
export class AppService {
constructor(
@InjectConfig()
private config: TypedConfig<AppConfig>,
) {}
connect() {
// TypeScript knows these are strings
const dbUrl = this.config.databaseUrl
const redis = this.config.redisHost
// TypeScript knows these might be undefined
const port = this.config.port || "3000"
}
}How It Works
Under the hood, @neoma/config uses a Proxy to intercept property access and automatically:
- Convert property names from camelCase to SCREAMING_SNAKE_CASE
- Look up the corresponding environment variable
- Return the value with proper typing
This means zero configuration, zero boilerplate, and full type safety.
Environment File Loading
Enable automatic .env file loading with the loadEnv option:
ConfigModule.forRoot({ loadEnv: true })File Loading Priority (highest to lowest):
- Existing
process.envvariables (from Docker, Kubernetes, shell, etc.) - Always wins .env.{NODE_ENV}.local- Environment-specific local overrides.env.local- Local overrides for all environments.env.{NODE_ENV}- Environment-specific configuration.env- Default configuration
Example:
# If DATABASE_URL is already set in process.env, it beats everything
export DATABASE_URL=postgres://from-environment/myapp # This always wins
# .env
DATABASE_URL=postgres://localhost/myapp
PORT=3000
# .env.local
DATABASE_URL=postgres://localhost/myapp_local # This wins over .env (but loses to process.env)
# .env.production
DATABASE_URL=postgres://prod-server/myapp
# .env.production.local
DATABASE_URL=postgres://localhost/myapp_prod_local # This wins over other files when NODE_ENV=productionImportant: Variables already set in process.env (from your deployment environment, Docker, shell exports, etc.) always take precedence over any .env file values.
Strict Mode
Enable strict mode to throw errors when accessing undefined environment variables:
ConfigModule.forRoot({ strict: true })This helps catch configuration errors early rather than silently returning undefined.
Example:
import { Module } from '@nestjs/common'
import { ConfigModule } from '@neoma/config'
@Module({
imports: [
// Enable strict mode
ConfigModule.forRoot({ strict: true })
],
})
export class AppModule {}
// In your service:
@Injectable()
export class PaymentService {
constructor(
@InjectConfig()
private config: TypedConfig<{
stripeApiKey: string
webhookSecret: string
}>
) {}
processPayment() {
// With strict mode: Throws error if STRIPE_API_KEY is not set
const key = this.config.stripeApiKey
// Error: "Strict mode error when accessing configuration property 'stripeApiKey'. STRIPE_API_KEY is not defined on process.env"
// Without strict mode: Returns undefined silently
const key = this.config.stripeApiKey // undefined
}
}Safe Property Access in Strict Mode
When using strict mode, you can safely check if a property exists before accessing it using the in operator:
@Injectable()
export class FlexibleService {
constructor(
@InjectConfig()
private config: TypedConfig<{
requiredApiKey: string
optionalFeatureFlag?: string
}>
) {}
initialize() {
// Safe existence check - won't throw in strict mode
if ('optionalFeatureFlag' in this.config) {
// Only access if it exists
const flag = this.config.optionalFeatureFlag
console.log('Feature flag enabled:', flag)
}
// Always access required properties directly
const apiKey = this.config.requiredApiKey // Throws if not set
}
}This pattern is especially useful for ecosystem packages that need to optionally integrate with other configuration while maintaining strict mode compatibility.
Type Coercion
Enable automatic type conversion from environment variable strings to JavaScript primitives:
ConfigModule.forRoot({ coerce: true })With coercion enabled, string values are automatically converted to their appropriate types:
Supported Conversions:
Booleans:
FEATURE_ENABLED=true # → true (boolean)
DEBUG_MODE=false # → false (boolean)Numbers:
PORT=3000 # → 3000 (number)
TIMEOUT=1.5 # → 1.5 (number)
WORKERS=0x10 # → 16 (hex)
MEMORY=1e6 # → 1000000 (scientific)Special Values:
CACHE_TTL=null # → null
DEFAULT_VALUE=undefined # → undefined
MAX_RETRIES=Infinity # → Infinity
INVALID_CONFIG=NaN # → NaNPreserved as Strings:
VERSION=007 # → "007" (leading zero preserved)
SPACING=" 123 " # → 123 (whitespace trimmed and converted)
EMPTY_VAL= # → "" (empty string preserved)
API_KEY=sk_test_123 # → "sk_test_123" (non-numeric stays string)Example Usage:
interface ServerConfig {
port: number // Coerced from "3000"
debug: boolean // Coerced from "true"
timeout: number // Coerced from "30"
apiKey: string // Stays as string
workers?: number // Coerced from "4" or undefined
}
@Module({
imports: [ConfigModule.forRoot({ coerce: true })]
})
export class AppModule {}
@Injectable()
export class ServerService {
constructor(
@InjectConfig()
private config: TypedConfig<ServerConfig>
) {}
start() {
const port = this.config.port // number: 3000
const debug = this.config.debug // boolean: true
const timeout = this.config.timeout // number: 30
// Type-safe operations
if (debug) console.log(`Starting on port ${port}`)
setTimeout(() => this.healthCheck(), timeout * 1000)
}
}Combining Options:
You can combine all options for a complete configuration solution:
ConfigModule.forRoot({
loadEnv: true, // Load .env files
strict: true, // Throw on missing required vars
coerce: true // Auto-convert types
})This gives you:
- Environment file loading with proper precedence
- Runtime validation for required variables
- Automatic type conversion for primitives
- Type safety with TypeScript interfaces
API Reference
ConfigModule
NestJS module that provides the ConfigService.
ConfigService<T>
Injectable service that provides typed access to environment variables.
TypedConfig<T>
Type helper that combines ConfigService with your configuration interface.
@InjectConfig()
Decorator for injecting the ConfigService into your services and controllers.
Links
License
MIT
