JSPM

  • Created
  • Published
  • Downloads 1154
  • Score
    100M100P100Q106408F
  • License Apache-2.0

The apt way to handle env

Package Exports

  • envapt

Readme

Envapt - The apt way to handle env

A powerful TypeScript-first environment configuration library that provides type detection, template variable resolution, and decorator-based class property configuration by extending dotenv.

Features

  • 🔧 Automatic Type Detection - Types inferred from fallback values
  • 🔗 Template Variables - ${VAR} syntax with circular reference protection
  • 🎯 Class Properties - Decorator-based configuration for class members
  • 🏷️ Built-in & Custom Converters - Ready-to-use converters for common patterns + custom transformations
  • 🌍 Environment Detection - Built-in development/staging/production handling
  • 📂 Multiple .env Files - Load from multiple sources
  • 💪 Edge Case Handling - Robust parsing for all scenarios
  • 🛡️ Type Safety - Full TypeScript support with proper type inference

Table of Contents

Requirements

  • Node.js: v22 or later (recommended for ESM and nodenext support)
  • TypeScript: v5.8 or later
  • Dependencies:
    • dotenv (runtime dependency | bundled)
  • TypeScript Compiler Options:
    • experimentalDecorators: true
    • module: nodenext OR esnext
    • moduleResolution: nodenext OR bundler
    • target: ESnext
    • lib: ESNext
  • ESM Support: Project uses ESM, so your environment and tooling should support ES modules.

Quick Start

Installation

# npm
npm install envapt

# pnpm
pnpm add envapt

# yarn
yarn add envapt

Basic Usage

Create a .env file:

APP_PORT=8443
APP_URL=http://localhost:${APP_PORT}
DATABASE_URL=postgres://localhost:5432/mydb
IS_PRODUCTION=false
MAX_CONNECTIONS=100
ALLOWED_ORIGINS=https://app.com,https://admin.com

Use with decorators (recommended):

import { Envapt, Envapter } from 'envapt';

// Global app configuration (static properties)
class AppConfig extends Envapter {
  @Envapt('APP_PORT', 3000)
  static readonly port: number;

  @Envapt('APP_URL', 'http://localhost:3000', 'url')
  static readonly url: URL;

  @Envapt('ALLOWED_ORIGINS', {
    fallback: ['http://localhost:3000'],
    converter: 'array'
  })
  static readonly allowedOrigins: string[];
}

// Service configuration (instance properties)
class DatabaseService {
  @Envapt('DATABASE_URL', 'sqlite://memory')
  declare readonly databaseUrl: string;

  @Envapt('MAX_CONNECTIONS', { converter: 'number', fallback: 10 })
  declare readonly maxConnections: number;

  @Envapt('REQUEST_TIMEOUT', { converter: 'time', fallback: 5000 })
  declare readonly timeout: number; // Converts "5s" to 5000ms

  async connect() {
    console.log(`Connecting to ${this.databaseUrl}`);
    // Connection logic here
  }
}

// Usage
console.log(AppConfig.port); // 8443 (number)
console.log(AppConfig.url.href); // "http://localhost:8443" (templated!)

const dbService = new DatabaseService();
await dbService.connect();

Or use functionally:
Limited to primitives, String, Number, Boolean, Symbol, and BigInt. Does not support converters.

import { Envapter } from 'envapt';

const port = Envapter.getNumber('APP_PORT', 3000);
const url = Envapter.get('APP_URL', 'http://localhost:3000');
const isProduction = Envapter.getBoolean('IS_PRODUCTION', false);

API Reference

Decorator API

The @Envapt decorator can be used on both static and instance class properties:

  • Static properties: Use for global configuration that's shared across your entire application (e.g., app port, global features, environment settings)
  • Instance properties: Use for service-specific configuration that may vary per service or when you want the configuration tied to a specific class instance (e.g., database connections, service endpoints, per-service settings)

Important: Instance properties must be declared with declare keyword or ! assertion since they're populated by the decorator rather than set in a constructor.

@Envapt('ENV_VAR', { fallback?: T, converter?: EnvConverter<T> })

Classic Syntax

@Envapt('ENV_VAR', fallback?, converter?)

Automatic Type Detection

Types are automatically inferred from fallback values. Use static properties for app-wide config and instance properties for service-specific config:

class Config extends Envapter {
  // Static properties for global settings
  @Envapt('APP_NAME', 'MyApp') // string
  static readonly appName: string;

  @Envapt('APP_PORT', 3000) // number
  static readonly port: number;

  @Envapt('DEBUG_MODE', false) // boolean
  static readonly debugMode: boolean;

  // Instance properties for service-specific settings
  @Envapt('SMTP_HOST', 'localhost') // string
  declare readonly smtpHost: string;

  @Envapt('SMTP_PORT', 587) // number
  declare readonly smtpPort: number;

  @Envapt('SMTP_SECURE', true) // boolean
  declare readonly smtpSecure: boolean;

  sendEmail(to: string, subject: string) {
    console.log(`Sending via ${this.smtpHost}:${this.smtpPort}`);
  }
}

Built-in Converters

Envapt provides many built-in converters for common patterns:

class Config extends Envapter {
  // Basic types
  @Envapt('APP_NAME', { converter: 'string', fallback: 'MyApp' })
  static readonly appName: string;

  @Envapt('PORT', { converter: 'number', fallback: 3000 })
  static readonly port: number;

  @Envapt('PRODUCTION_MODE', { converter: 'boolean', fallback: false })
  static readonly productionMode: boolean;

  // Advanced types
  @Envapt('CORS_ORIGINS', { converter: 'array', fallback: [] })
  static readonly corsOrigins: string[];

  @Envapt('CONFIG_JSON', { converter: 'json', fallback: {} })
  static readonly config: object;

  @Envapt('API_URL', { converter: 'url', fallback: new URL('http://localhost') })
  static readonly apiUrl: URL;

  @Envapt('TIMEOUT', { converter: 'time', fallback: 5000 })
  static readonly timeout: number; // Converts "30s" to 30000ms

  // Instance properties work the same way
  @Envapt('CACHE_TTL', { converter: 'time', fallback: 3600000 })
  declare readonly cacheTtl: number; // "1h" becomes 3600000ms
}

Available Built-in Converters:

  • 'string' - String values
  • 'number' - Numeric values (integers and floats)
  • 'integer' - Integer values only
  • 'float' - Float values only
  • 'boolean' - Boolean values (true/false, yes/no, on/off, 1/0)
  • 'bigint' - BigInt values for large integers
  • 'symbol' - Symbol values (creates symbols from string descriptions)
  • 'time' - Time values (converts "5s", "30m", "2h" to milliseconds)
  • 'json' - JSON objects/arrays (safe parsing with fallback)
  • 'array' - Comma-separated string arrays
  • 'url' - URL objects
  • 'regexp' - Regular expressions (supports /pattern/flags syntax)
  • 'date' - Date objects (supports ISO strings and timestamps)
  • 'time' - Values denoting time. Such as 100, 30ms, 5s, 10m, 1.5h. (parsed to ms)

Custom Array Converters

For more control over array parsing:

class Config extends Envapter {
  // Basic array (comma-separated strings)
  @Envapt('TAGS', { converter: 'array', fallback: [] })
  static readonly tags: string[];

  // Custom delimiter
  @Envapt('ALLOWED_METHODS', { converter: { delimiter: '|' }, fallback: ['GET'] })
  declare readonly allowedMethods: string[];

  // Custom delimiter with type conversion
  @Envapt('RATE_LIMITS', { converter: { delimiter: ',', type: 'number' }, fallback: [100] })
  declare readonly rateLimits: number[];

  @Envapt('FEATURE_FLAGS', { converter: { delimiter: ';', type: 'boolean' }, fallback: [false] })
  declare readonly featureFlags: boolean[];
}

ArrayConverter Interface:

  • delimiter: string - The string used to split array elements
  • type?: BuiltInConverter - Optional type to convert each element to (excludes 'array', 'json', and 'regexp')

Custom Converters

Transform environment values to any type:

class Config extends Envapter {
  @Envapt('TAGS', {
    fallback: new Set(['default']),
    converter: (raw, fallback) => {
      if (!raw) return fallback;
      return new Set(raw.split(',').map((s) => s.trim()));
    }
  })
  static readonly tags: Set<string>;

  @Envapt('NOTIFICATION_CHANNELS', {
    fallback: new Map([['email', 'enabled']]),
    converter: (raw, fallback) => {
      if (!raw) return fallback;
      const map = new Map();
      raw.split(',').forEach((pair) => {
        const [key, value] = pair.split(':');
        map.set(key?.trim(), value?.trim() || 'enabled');
      });
      return map;
    }
  })
  declare readonly channels: Map<string, string>;
}

Handling Missing Values

Control what happens when environment variables don't exist:

class Config extends Envapter {
  // Returns undefined if not found
  @Envapt('OPTIONAL_FEATURE', { fallback: undefined })
  static readonly optionalFeature: string | undefined;

  // Returns null if not found (no fallback provided)
  @Envapt('MISSING_CONFIG', { converter: 'string' })
  static readonly missingConfig: string | null;

  // Uses fallback if not found
  @Envapt('DEFAULT_THEME', { fallback: 'light' })
  static readonly defaultTheme: string;

  // Instance properties work the same way
  @Envapt('LOG_FILE_PATH', { fallback: undefined })
  declare readonly logFilePath: string | undefined;
}

Functional API

import { Envapter } from 'envapt';

// Type-specific getters
const str = Envapter.get('STRING_VAR', 'default');
const num = Envapter.getNumber('NUMBER_VAR', 42);
const bool = Envapter.getBoolean('BOOLEAN_VAR', false);

// Instance methods (same API)
const envapter = new Envapter();
const value = envapter.get('VAR', 'default');

Environment Detection

Envapt automatically detects your environment from these variables (in order):

  1. ENVIRONMENT
  2. ENV
  3. NODE_ENV

Supported values: development, staging, production (case-sensitive)

Environment Management

import { Envapter, EnvaptEnvironment } from 'envapt';

// Check current environment
console.log(Envapter.environment); // Environment.Development
console.log(Envapter.isProduction); // false
console.log(Envapter.isDevelopment); // true
console.log(Envapter.isStaging); // false

// Set environment
Envapter.environment = EnvaptEnvironment.Production;
Envapter.environment = 'staging'; // string also works

Multiple .env Files

import { resolve } from 'node:path';
import { Envapter } from 'envapt';

// Load from multiple files
Envapter.envPaths = [resolve(__dirname, '.env.local'), resolve(__dirname, '.env.production')];

// Or single file
Envapter.envPaths = resolve(__dirname, '.env.production');

// Or just don't set a path for it to default to .env at the root of your project

Template Variables

Envapt supports variable interpolation with ${VARIABLE} syntax:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_URL=postgres://${DATABASE_HOST}:${DATABASE_PORT}/mydb

API_VERSION=v1
API_BASE=https://api.example.com
API_ENDPOINT=${API_BASE}/${API_VERSION}/users

Circular Reference Protection

CIRCULAR_A=${CIRCULAR_B}
CIRCULAR_B=${CIRCULAR_A}

Circular references are detected and preserved as-is rather than causing infinite loops.

Advanced Examples

Complex Configuration

import { Envapt, Envapter } from 'envapt';

class AppConfig extends Envapter {
  // Global settings (static)
  @Envapt('PORT', 3000)
  static readonly port: number;

  @Envapt('REQUEST_TIMEOUT', { converter: 'time', fallback: 10000 })
  static readonly requestTimeout: number; // "5s" -> 5000ms (if env is set to "5s")

  @Envapt('FEATURE_FLAGS', {
    fallback: new Set(['basic']),
    converter: (raw, fallback) => {
      if (!raw) return fallback;
      return new Set(raw.split(',').map((s) => s.trim()));
    }
  })
  static readonly featureFlags: Set<string>;

  // Service settings (instance)
  @Envapt('DB_URL', 'sqlite://memory')
  declare readonly databaseUrl: string;

  @Envapt('CACHE_TTL', { converter: 'time', fallback: 3600000 })
  declare readonly cacheTtl: number; // "1h" -> 3600000ms

  @Envapt('REDIS_URLS', {
    fallback: [new URL('redis://localhost:6379')],
    converter: (raw, fallback) => (raw ? raw.split(',').map((s) => new URL(s)) : fallback)
  })
  declare readonly redisUrls: URL[];

  async initialize() {
    console.log(`App running on port ${AppConfig.port}`);
    console.log(`Database: ${this.databaseUrl}`);
    console.log(`Cache TTL: ${this.cacheTtl}ms`);
  }
}