@funeste38/envaptex
v1.0.0
Published
The apt way to handle env
Maintainers
Readme
✨ Features
- 🔧 Automatic Type Detection - Runtime types inferred from fallback values
- 🔗 Template Variables -
${VAR}syntax with circular reference protection - 🎯 Class Properties - Functional and Decorator-based configuration for class members (Decorators: TypeScript only)
- 🏷️ Built-in & Custom Converters - Ready-to-use converters for common patterns + custom transformations
- 🔖 Tagged Template Resolver - Tagged template literals with environment variable resolution
- 🌍 Environment Detection - Built-in development/staging/production handling
- 💪 Edge Case Handling - Robust validation and parsing for all scenarios
- 🛡️ Type Safety - Full TypeScript support with proper type inference (TypeScript optional)
- 📂 Multiple .env Files - Load from multiple sources
- ⚡ Lightweight - Minimal overhead with
dotenvbundled
Table of Contents
⚙️ Essentials
🧬 API Reference
🌍 Environment & Templates
🛠 Configuration & Errors
🚀 Examples
Requirements
TypeScript Users Only
- TypeScript:
>=5.8(Only required for decorator API)
// tsconfig.json (required settings for decorators)
{
"experimentalDecorators": true,
"module": "esnext", // or "nodenext"
"moduleResolution": "bundler", // or "nodenext"
"target": "ESNext",
"lib": ["ESNext"]
}[!NOTE] JavaScript users can use all features except the
@Envaptdecorator API. The Functional API, Tagged Template Resolver, and all converters work perfectly in plain JavaScript.
Quick Start
Installation
| Package Manager | Command |
| --------------- | --------------------------------- |
| pnpm | pnpm add envapt |
| yarn | yarn add envapt |
| npm | npm install envapt |
| deno (jsr) | deno add jsr:@materwelon/envapt |
| deno (npm) | deno add npm:envapt |
| bun | bun add envapt |
Basic Usage
Step 1: 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.comJavaScript Example (Functional API):
import { Envapter, Converters } from 'envapt';
// Basic usage
const port = Envapter.getNumber('APP_PORT', 3000);
const url = Envapter.get('APP_URL', 'http://localhost:3000');
const isProduction = Envapter.isProduction;
console.log(`Server running on port ${port}`); // 8443
console.log(`URL: ${url}`); // "http://localhost:8443"
// Advanced converters
const corsOrigins = Envapter.getUsing('ALLOWED_ORIGINS', Converters.Array, []);
const dbConfig = Envapter.getUsing('DATABASE_CONFIG', Converters.Json, {});
// Tagged template literals
const message = Envapter.resolve`Server ${'APP_URL'} is ready!`;
console.log(message); // "Server http://localhost:8443 is ready!"TypeScript Example (Decorator API):
import { Envapt, Envapter, Converters } from 'envapt';
// Global app configuration (static properties)
class AppConfig extends Envapter {
@Envapt('APP_PORT', 3000)
static readonly port: number;
// The Classic Syntax only works for Primitive Converters. Converters.Url is a Built-in Converter.
@Envapt('APP_URL', { fallback: new URL('http://localhost:3000'), converter: Converters.Url })
static readonly url: URL;
@Envapt('ALLOWED_ORIGINS', {
fallback: ['http://localhost:3000'],
converter: Converters.Array
})
static readonly allowedOrigins: string[];
}
// Service configuration (instance properties)
class DatabaseService {
@Envapt('DATABASE_URL', 'sqlite://memory')
declare readonly databaseUrl: string;
// Will detect that '10' is a number and set the runtime type accordingly
@Envapt('MAX_CONNECTIONS', 10)
declare readonly maxConnections: number;
@Envapt('REQUEST_TIMEOUT', { converter: Converters.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"
const dbService = new DatabaseService();
await dbService.connect();API Reference
Decorator API
[!IMPORTANT] TypeScript Only: The
@Envaptdecorator API requires TypeScript withexperimentalDecorators: true. JavaScript users should use the Functional API instead.
The @Envapt decorator can be used on both static and instance class properties:
- Static properties: Can use for global configuration that's shared across your entire application (e.g., app port, global features, environment settings)
- Instance properties: Can 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.
Modern Syntax (Recommended)
@Envapt('ENV_VAR', { fallback?: T, converter?: EnvConverter<T> })[!TIP] Generic Typing for Better IntelliSense
You can specify explicit types using generics for better type safety and IntelliSense:
// Explicit typing provides better IntelliSense for complex types @Envapt<DatabaseConfig>('DB_CONFIG', { fallback: { host: 'localhost', port: 5432, ssl: false }, converter: Converters.Json }) static readonly dbConfig: DatabaseConfig;
Classic Syntax
@Envapt('ENV_VAR', fallback?, converter?)Automatic Runtime Type Detection
Types are automatically inferred from fallback values.
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}`);
}
}Primitive Converters
Envapt allows using the 5 "primitive" type-like converters. These will coerce values.
[!NOTE] The runtime validator will ignore this usage, allowing type coercion for flexibility.
Valid Primitive Types: String, Number, Boolean, Symbol, and BigInt.
class Config extends Envapter {
@Envapt('PORT_STRING', { fallback: 'hello-world', converter: String })
static readonly portAsString: string;
@Envapt('DEBUG_FLAG', { fallback: true, converter: Boolean })
static readonly debugMode: boolean;
@Envapt('USER_ID', { fallback: 12345, converter: Number })
static readonly userId: number;
@Envapt('MAX_SAFE_INT', { fallback: 9007199254740991n, converter: BigInt })
static readonly maxSafeInt: bigint;
@Envapt('APP_INSTANCE', { fallback: Symbol(main), converter: Symbol })
static readonly appInstance: symbol;
// Instance properties work the same way
@Envapt('CONNECTION_TIMEOUT', { fallback: 5000, converter: Number })
declare readonly timeout: number;
// Type coercion example
@Envapt('PERMISSIONS', { fallback: '72394823472342983', converter: BigInt })
declare readonly permissions: bigint; // Converts "72394823472342983" to BigInt
}When to use primitive converters:
- When you need explicit type coercion between incompatible types
- When working with external systems that provide values in unexpected formats
Built-in Converters
Envapt provides many built-in converters for common patterns:
[!IMPORTANT] Use the
Convertersenum instead of string literals. They look better, and provide better type inference:import { Converters } from 'envapt'; // ✅ Recommended: Use enum @Envapt('PORT', { converter: Converters.Number, fallback: 3000 }) // ❌ Discouraged: String literals (still supported for compatibility) @Envapt('PORT', { converter: 'number', fallback: 3000 })Built-in converters enforce strict type validation between the converter and fallback types. The converter's expected return type must match the fallback's type.
class Config extends Envapter {
// Basic types
@Envapt('APP_NAME', { converter: Converters.String, fallback: 'MyApp' })
static readonly appName: string;
@Envapt('PORT', { converter: Converters.Number, fallback: 3000 })
static readonly port: number;
@Envapt('PRODUCTION_MODE', { converter: Converters.Boolean, fallback: false })
static readonly productionMode: boolean;
// Advanced types
@Envapt('CORS_ORIGINS', { converter: Converters.Array, fallback: [] })
static readonly corsOrigins: string[];
@Envapt('CONFIG_JSON', { converter: Converters.Json, fallback: {} })
static readonly config: object;
@Envapt('API_URL', { converter: Converters.Url, fallback: new URL('http://localhost') })
static readonly apiUrl: URL;
@Envapt('TIMEOUT', { converter: Converters.Time, fallback: 5000 })
static readonly timeout: number; // Converts "30s" to 30000ms
// Instance properties work the same way
@Envapt('CACHE_TTL', { converter: Converters.Time, fallback: 3600000 })
declare readonly cacheTtl: number; // "1h" becomes 3600000ms
}[!WARNING] These will throw runtime errors due to type mismatches:
// ❌ String converter with number fallback @Envapt('VAR', { converter: Converters.String, fallback: 42 }) // ❌ URL converter with string fallback @Envapt('VAR', { converter: Converters.Url, fallback: 'http://example.com' }) // ✅ Use primitive constructors for type coercion instead @Envapt('VAR', { converter: String, fallback: 42 })
Available Built-in Converters:
| Converter | Alias | Description |
| -------------------- | ----------- | -------------------------------------------------------------------- |
| Converters.String | 'string' | String values |
| Converters.Number | 'number' | Numeric values (integers and floats) |
| Converters.Integer | 'integer' | Integer values only |
| Converters.Float | 'float' | Float values only |
| Converters.Boolean | 'boolean' | Boolean values (true/false, yes/no, on/off, 1/0) |
| Converters.Bigint | 'bigint' | BigInt values for large integers |
| Converters.Symbol | 'symbol' | Symbol values (creates symbols from string descriptions) |
| Converters.Json | 'json' | JSON objects/arrays (safe parsing with fallback) |
| Converters.Array | 'array' | Comma-separated string arrays |
| Converters.Url | 'url' | URL objects |
| Converters.Regexp | 'regexp' | Regular expressions (supports /pattern/flags syntax) |
| Converters.Date | 'date' | Date objects (supports ISO strings and timestamps) |
| Converters.Time | 'time' | Time values (e.g. "5s", "30m", "2h" converted to milliseconds) |
Custom Array Converters
For more control over array parsing:
[!IMPORTANT] Array converters validate that:
- Fallback must be an array (if provided)
- All fallback elements have consistent types (no mixed types like
['string', 42, true])- Array converter
typematches fallback element types (iftypeis specified)
class Config extends Envapter {
// Basic array (comma-separated strings)
@Envapt('TAGS', { converter: Converters.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: Converters.Number }, fallback: [100] })
declare readonly rateLimits: number[];
@Envapt('FEATURE_FLAGS', { converter: { delimiter: ';', type: 'boolean' }, fallback: [false] })
declare readonly featureFlags: boolean[];
}[!WARNING] These will throw runtime validation errors:
// ❌ Mixed types in fallback array @Envapt('MIXED', { converter: Converters.Array, fallback: ['string', 42, true] }) // ❌ Array converter type doesn't match fallback elements @Envapt('NUMS', { converter: { delimiter: ',', type: Converters.Number }, fallback: ['not', 'numbers'] }) // ❌ Non-array fallback with array converter @Envapt('INVALID', { converter: Converters.Array, fallback: 'not-an-array' })
ArrayConverter Interface:
delimiter: string- The string used to split array elementstype?: BuiltInConverter- Optional type to convert each element to (excludesConverters.Array,Converters.Json, andConverters.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>;
}[!TIP] Custom Validation with Error Throwing
Custom converters can throw errors for validation. The custom converter is called even when a variable is not found in the env file(s):
@Envapt<string>('API_KEY', { converter(raw, _fallback) { if (typeof raw !== 'string' || raw === '') { throw new Error('API_KEY is required and cannot be empty'); } return raw; } }) static readonly apiKey: string;No
fallbackneeded here because the converter throws an error if a value is not what we want it to be
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: Converters.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
For functional-style environment variable access on primitive types:
import { Envapter, Converters } from 'envapt';
// Basic type-specific getters
const str = Envapter.get('STRING_VAR', 'default');
const num = Envapter.getNumber('NUMBER_VAR', 42);
const bool = Envapter.getBoolean('BOOLEAN_VAR', false);
const bigint = Envapter.getBigInt('BIGINT_VAR', 100n);
const symbol = Envapter.getSymbol('SYMBOL_VAR', Symbol('default'));
// Advanced converter methods
const jsonData = Envapter.getUsing('CONFIG_JSON', Converters.Json);
const urlArray = Envapter.getUsing('API_URLS', { delimiter: ',', type: Converters.Url });
const customData = Envapter.getWith('RAW_DATA', (raw) => raw?.split('|').map((s) => s.trim()));
// Instance methods (same API available)
const envapter = new Envapter();
const value = envapter.get('VAR', 'default');
const processed = envapter.getUsing('DATA', Converters.Array);For functional-style environment variable access with converters:
import { Envapter, Converters } from 'envapt';
// Use built-in converters directly
const config = Envapter.getUsing('API_CONFIG', Converters.Json, { default: 'value' });
const urls = Envapter.getUsing('SERVICE_URLS', { delimiter: '|', type: Converters.Url });
// TypeScript: Use type override for better type inference
const typedConfig = Envapter.getUsing<{ host: string; port: number; ssl: boolean }>('DATABASE_CONFIG', Converters.Json);
// typedConfig is now typed as { host: string; port: number; ssl: boolean } instead of JsonValue | undefined
// Use custom converter functions
const processedData = Envapter.getWith(
'RAW_DATA',
(raw, fallback) => {
if (!raw) return fallback ?? [];
return raw.split(',').map((item) => ({ name: item.trim(), enabled: true }));
},
[]
);
// Instance methods work the same way
const envapter = new Envapter();
const result = envapter.getUsing('DATABASE_CONFIG', Converters.Json);[!TIP] Type Override with
getUsingYou can explicitly specify the return type for
getUsingwhen TypeScript's inference isn't specific enough (especially useful withConverters.Json):// Default behavior const config = Envapter.getUsing('CONFIG', Converters.Json); // type: JsonValue | undefined (undefined because no fallback) // Override with specific interface interface DatabaseConfig { host: string; port: number; ssl: boolean; } const dbConfig = Envapter.getUsing<DatabaseConfig>('DB_CONFIG', Converters.Json); // dbConfig is now properly typed as DatabaseConfigMake sure the fallback value matches the expected type, if you use a fallback. Otherwise you'll see a TypeScript error.
This does NOT validate the type at runtime. You'll need to handle that yourself.
Converter Type Quick Reference
| Use Case | Converter Type | Example |
| ----------------------- | ------------------------- | --------------------------------------------------------- |
| Type coercion | Primitive constructors | converter: String |
| Strict validation | Built-in converters | converter: Converters.String |
| Array parsing | Built-in Array converters | converter: { delimiter: ',', type?: Converters.String } |
| Complex transforms | Custom function | converter: (raw, fallback) => ... |
| Functional built-in | getUsing() method | Envapter.getUsing('VAR', Converters.Json) |
| Type override | getUsing<T>() method | Envapter.getUsing<MyType>('VAR', Converters.Json) |
| Functional custom | getWith() method | Envapter.getWith('VAR', (raw) => transform(raw)) |
[!TIP] Use the
Convertersenum. They look better. Start with built-in converters, use primitive constructors when you need coercion, and custom converters for complex transforms.
Tagged Template Resolver
Envapt provides a convenient tagged template literal syntax for resolving environment variables directly in template strings:
import { Envapter } from 'envapt';
// Given these environment variables:
// API_HOST=api.example.com
// API_PORT=8080
// API_URL=https://${API_HOST}:${API_PORT}
// SERVICE_NAME=UserService
// Use tagged template literals for string interpolation
const endpoint = Envapter.resolve`Connecting to ${'SERVICE_NAME'} at ${'API_URL'}`;
// Returns: "Connecting to UserService at https://api.example.com:8080"
const logMessage = Envapter.resolve`Starting ${'SERVICE_NAME'} on port ${'API_PORT'}`;
// Returns: "Starting UserService on port 8080"
// Works with instance methods too
const envapter = new Envapter();
const status = envapter.resolve`${'SERVICE_NAME'} is running`;
// Returns: "UserService is running"Works seamlessly with template variables in your .env file:
# Your .env file
API_HOST=api.example.com
API_PORT=8080
API_URL=https://${API_HOST}:${API_PORT} # Template resolved first
SERVICE_NAME=UserServiceconst message = Envapter.resolve`Service ${'SERVICE_NAME'} endpoint: ${'API_URL'}`;
// Returns: "Service UserService endpoint: https://api.example.com:8080"[!NOTE] Tagged template literals work with any environment variables, including those that use
${VAR}template syntax in your.envfile. The template resolution happens first, then the tagged template interpolation.
Environment Detection
Envapt automatically detects your environment from these variables (in order):
ENVIRONMENTENVNODE_ENV
Supported values: development, staging, production (case-sensitive)
Environment Management
import { Envapter, EnvaptEnvironment } from 'envapt';
// Check current environment
console.log(Envapter.environment); // Environment.Development (default)
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 worksConfiguration
Multiple .env Files
import { resolve } from 'node:path';
import { Envapter } from 'envapt';
// Load from multiple files
Envapter.envPaths = [resolve(import.meta.dirname, '.env.local'), resolve(import.meta.dirname, '.env.production')];
// Or single file
Envapter.envPaths = resolve(import.meta.dirname, '.env.production');
// Or just don't set a path for it to default to .env at the root of your project
// Also, in CommonJS, use `__dirname` instead of `import.meta.dirname`:Dotenv Configuration
Envapt allows you to customize dotenv behavior by setting configuration options:
import { Envapter } from 'envapt';
// Set dotenv configuration options
Envapter.dotenvConfig = {
encoding: 'latin1', // File encoding (default: 'utf8')
debug: true, // Enable debug logging
override: true, // Override existing environment variables
quiet: false, // Suppress non-error output (default: true)
DOTENV_KEY: 'key...' // Decryption key for .env.vault files
};
// Get current configuration
console.log(Envapter.dotenvConfig);[!NOTE] The
pathandprocessEnvoptions are managed internally by Envapter and cannot be set viadotenvConfig.
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}/usersCircular Reference Protection
CIRCULAR_A=${CIRCULAR_B}
CIRCULAR_B=${CIRCULAR_A}Circular references are detected and preserved as-is rather than causing infinite loops.
Error Handling
Envapt provides detailed error codes for better debugging and error handling:
import { EnvaptError, EnvaptErrorCodes } from 'envapt';
try {
// This will throw an error for invalid configuration
Envapter.dotenvConfig = { path: '.env.custom' };
} catch (error) {
if (error instanceof EnvaptError) {
console.log('Error code:', error.code);
console.log('Error message:', error.message);
// Handle specific error types
switch (error.code) {
case EnvaptErrorCodes.InvalidUserDefinedConfig:
console.log('Invalid configuration provided');
break;
case EnvaptErrorCodes.EnvFileNotFound:
console.log('Environment file not found');
break;
default:
console.warn('Unhandled error code:', error.code);
break;
}
}
}Error Code Reference
🔧 Fallback Errors (1xx)
| Error Code | Description |
| ---------------------------------------- | --------------------------------------------------------- |
| InvalidFallback (101) | Invalid fallback value provided |
| InvalidFallbackType (102) | Fallback value type doesn't match expected converter type |
| ArrayFallbackElementTypeMismatch (103) | Array fallback contains elements of wrong type |
| FallbackConverterTypeMismatch (104) | Fallback type doesn't match the specified converter |
🧪 Converter Errors (2xx)
| Error Code | Description |
| --------------------------------- | ---------------------------------------------- |
| InvalidArrayConverterType (201) | Invalid array converter configuration provided |
| InvalidBuiltInConverter (202) | Invalid built-in converter specified |
| InvalidCustomConverter (203) | Custom converter function is invalid |
| InvalidConverterType (204) | Converter type is not recognized |
| PrimitiveCoercionFailed (205) | Primitive type coercion failed |
📂 Environment File & Config Errors (3xx)
| Error Code | Description |
| -------------------------------- | ---------------------------------------------- |
| MissingDelimiter (301) | Delimiter is missing in array converter config |
| InvalidUserDefinedConfig (302) | Invalid user-defined configuration provided |
| EnvFilesNotFound (303) | Specified environment file doesn't exist |
Advanced Examples
JavaScript
import { Envapter, Converters } from 'envapt';
// Global configuration
const config = {
port: Envapter.getNumber('PORT', 3000),
requestTimeout: Envapter.getUsing('REQUEST_TIMEOUT', Converters.Time, 10000), // "5s" -> 5000ms
featureFlags: Envapter.getWith(
'FEATURE_FLAGS',
(raw, fallback) => {
if (!raw) return fallback;
return new Set(raw.split(',').map((s) => s.trim()));
},
new Set(['basic'])
)
};
// Service configuration
class DatabaseService {
constructor() {
this.databaseUrl = Envapter.get('DB_URL', 'sqlite://memory');
this.cacheTtl = Envapter.getUsing('CACHE_TTL', Converters.Time, 3600000); // "1h" -> 3600000ms
this.redisUrls = Envapter.getWith(
'REDIS_URLS',
(raw, fallback) => (raw ? raw.split(',').map((s) => new URL(s)) : fallback),
[new URL('redis://localhost:6379')]
);
}
async initialize() {
console.log(`App running on port ${config.port}`);
console.log(`Database: ${this.databaseUrl}`);
console.log(`Cache TTL: ${this.cacheTtl}ms`);
}
}TypeScript
import { Envapt, Envapter, Converters } from 'envapt';
class AppConfig extends Envapter {
// Global settings (static)
@Envapt('PORT', 3000)
static readonly port: number;
@Envapt('REQUEST_TIMEOUT', { converter: Converters.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: Converters.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`);
}
}