npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@neoma/config

v0.3.0

Published

Simple, type-safe environment configuration for NestJS applications

Downloads

112

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/config

Basic 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_NAME

Type 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:

  1. Convert property names from camelCase to SCREAMING_SNAKE_CASE
  2. Look up the corresponding environment variable
  3. 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):

  1. Existing process.env variables (from Docker, Kubernetes, shell, etc.) - Always wins
  2. .env.{NODE_ENV}.local - Environment-specific local overrides
  3. .env.local - Local overrides for all environments
  4. .env.{NODE_ENV} - Environment-specific configuration
  5. .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=production

Important: 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     # → NaN

Preserved 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