envapt 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,987 +1,99 @@
1
- <p align="center">
2
- <img src="/.github/assets/banner.png" alt="Envapt – The apt way to handle environment variables" width="100%" />
3
- </p>
4
-
5
- <p align="center">
6
- A JavaScript/TypeScript environment configuration library that eliminates the boilerplate of transforming parsed <code>.env</code><br/>
7
- Get environment variables with correct runtime typing & fallbacks, template support, automatic, built-in, & custom transformations, and a tagged template resolver.<br/>
8
- <strong>No more <code>process.env.PORT || '3000'</code> everywhere!</strong>
9
- </p>
10
- <div align="center">
1
+ <table border="0" cellspacing="0" cellpadding="0">
2
+ <tr>
3
+ <td width="160" valign="middle" align="center">
4
+ <img src="https://raw.githubusercontent.com/materwelonDhruv/envapt/main/.github/assets/logo.png" width="120" alt="envapt logo" />
5
+ </td>
6
+ <td valign="middle">
7
+ <h1>envapt</h1>
8
+ <p>
9
+ <strong>The apt way to handle environment variables.</strong><br/>
10
+ Read them as typed values, with zero runtime dependencies and the same API on Node, Bun, and Deno.
11
+ </p>
11
12
  <a href="https://www.npmjs.com/package/envapt"><img alt="npm" src="https://img.shields.io/npm/v/envapt?logo=npm&logoColor=cb3838&label=%20&labelColor=103544&color=cb3838"></a>
13
+ <a href="https://www.npmjs.com/package/envapt"><img alt="downloads" src="https://img.shields.io/npm/dm/envapt?style=flat&color=f7f6e8&labelColor=103544&label=downloads"></a>
12
14
  <a href="https://jsr.io/@materwelon/envapt"><img alt="jsr" src="https://jsr.io/badges/@materwelon/envapt"></a>
13
- <img alt="CI" src="https://img.shields.io/github/actions/workflow/status/materwelonDhruv/envapt/check.yml?branch=main&label=tests&style=flat&logo=github&color=3fb950&labelColor=103544"></a>
14
- <a href="https://codecov.io/github/materwelonDhruv/envapt"><img alt="codecov" src="https://img.shields.io/codecov/c/github/materwelonDhruv/envapt/main?token=IQ4GC645LO&logo=codecov&color=f01f7a&labelColor=103544"/></a>
15
- <br>
15
+ <img alt="CI" src="https://img.shields.io/github/actions/workflow/status/materwelonDhruv/envapt/checks.yml?branch=main&label=tests&style=flat&logo=github&color=3fb950&labelColor=103544">
16
16
  <a href="LICENSE"><img alt="License" src="https://img.shields.io/npm/l/envapt?style=flat&color=e97826&logo=apache&label="></a>
17
- <a href="https://www.typescriptlang.org/"><img alt="Types" src="https://img.shields.io/badge/TypeScript-3178c6?style=flat&logo=typescript&logoColor=white"></a>
18
- <a href="https://nodejs.org/api/esm.html"><img alt="ESM" src="https://img.shields.io/badge/ESM-ffca28?style=flat"></a>
19
- <a href="https://nodejs.org/api/modules.html"><img alt="CJS" src="https://img.shields.io/badge/CJS-ff6b35?style=flat"></a>
20
- <a href="https://nodejs.org/"><img alt="Node" src="https://img.shields.io/badge/node-%3E=22.0.0-339933?style=flat&logo=node.js&logoColor=white"></a>
21
- <a href="https://bundlephobia.com/package/envapt"><img alt="Bundle Size" src="https://img.shields.io/bundlephobia/minzip/envapt?style=flat&color=212121"></a>
22
- <a href="https://www.npmjs.com/package/envapt"><img alt="Downloads" src="https://img.shields.io/npm/dm/envapt?style=flat&color=f7f6e8"></a>
23
- <a href="https://github.com/materwelonDhruv/envapt/stargazers"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/materwelondhruv/envapt?style=flat&color=e3b341"></a>
24
- </div>
25
-
26
- ---
27
-
28
- ## ✨ Features
29
-
30
- - 🔧 **Automatic Type Detection** - Runtime types inferred from fallback values
31
- - 🔗 **Template Variables** - `${VAR}` syntax with circular reference protection
32
- - 🎯 **Class Properties** - Functional and Decorator-based configuration for class members _(Decorators: TypeScript only)_
33
- - 🏷️ **Built-in & Custom Converters** - Ready-to-use converters for common patterns + custom transformations
34
- - 🔖 **Tagged Template Resolver** - Tagged template literals with environment variable resolution
35
- - 🌍 **Environment Detection** - Built-in development/staging/production handling
36
- - 💪 **Edge Case Handling** - Robust validation and parsing for all scenarios
37
- - 🔑 **Multi-Key Lookups** - Provide ordered lists of env keys and Envapt will use the first value it finds
38
- - 🛡️ **Type Safety** - Full TypeScript support with proper type inference _(TypeScript optional)_
39
- - 📂 **Multiple .env Files** - Load from multiple sources
40
- - ⚡ **Lightweight** - Minimal overhead with [`dotenv`](https://www.npmjs.com/package/dotenv) bundled
17
+ </td>
18
+ </tr>
19
+ </table>
41
20
 
42
21
  ---
43
22
 
44
- ## Table of Contents
45
-
46
- ### ⚙️ Essentials
47
-
48
- - [Requirements](#requirements)
49
- - [Quick Start](#quick-start)
50
- - [Installation](#installation)
51
- - [Basic Usage](#basic-usage)
52
-
53
- ### 🧬 API Reference
54
-
55
- - [API Reference](#api-reference)
56
- - [Decorator API](#decorator-api)
57
- - [Modern Syntax (Recommended)](#modern-syntax-recommended)
58
- - [Classic Syntax](#classic-syntax)
59
- - [Automatic Runtime Type Detection](#automatic-runtime-type-detection)
60
- - [Primitive Converters](#primitive-converters)
61
- - [Built-in Converters](#built-in-converters)
62
- - [Custom Array Converters](#custom-array-converters)
63
- - [Custom Converters](#custom-converters)
64
- - [Handling Missing Values](#handling-missing-values)
65
- - [Functional API](#functional-api)
66
- - [Tagged Template Resolver](#tagged-template-resolver)
67
- - [Converter Type Quick Reference](#converter-type-quick-reference)
68
-
69
- ### 🌍 Environment & Templates
70
-
71
- - [Environment Detection](#environment-detection)
72
- - [Environment Management](#environment-management)
73
- - [Template Variables](#template-variables)
74
- - [Circular Reference Protection](#circular-reference-protection)
75
-
76
- ### 🛠 Configuration & Errors
77
-
78
- - [Configuration](#configuration)
79
- - [Multiple .env Files](#multiple-env-files)
80
- - [Dotenv Configuration](#dotenv-configuration)
81
- - [Error Handling](#error-handling)
82
- - [Error Code Reference](#error-code-reference)
83
-
84
- ### 🚀 Examples
85
-
86
- - [Advanced Examples](#advanced-examples)
87
- - [JavaScript](#javascript)
88
- - [TypeScript](#typescript)
89
-
90
- <div align="right">
91
-
92
- **[⬆️ Back to Top](#table-of-contents)**
93
-
94
- </div>
95
-
96
- ---
97
-
98
- ## Requirements
99
-
100
- ### TypeScript Users Only
101
-
102
- - **TypeScript**: `>=5.8` _(Only required for decorator API)_
103
-
104
- ```jsonc
105
- // tsconfig.json (required settings for decorators)
106
- {
107
- "experimentalDecorators": true,
108
- "module": "esnext", // or "nodenext"
109
- "moduleResolution": "bundler", // or "nodenext"
110
- "target": "ESNext",
111
- "lib": ["ESNext"]
112
- }
113
- ```
114
-
115
- > [!NOTE]
116
- > **JavaScript users** can use all features except the `@Envapt` decorator API. The [Functional API](#functional-api), [Tagged Template Resolver](#tagged-template-resolver), and all converters work perfectly in plain JavaScript.
117
-
118
- <div align="right">
119
-
120
- **[⬆️ Back to Top](#table-of-contents)**
121
-
122
- </div>
123
-
124
- ## Quick Start
125
-
126
- ### Installation
127
-
128
- | Package Manager | Command |
129
- | --------------- | --------------------------------- |
130
- | **pnpm** | `pnpm add envapt` |
131
- | **yarn** | `yarn add envapt` |
132
- | **npm** | `npm install envapt` |
133
- | **deno** (jsr) | `deno add jsr:@materwelon/envapt` |
134
- | **deno** (npm) | `deno add npm:envapt` |
135
- | **bun** | `bun add envapt` |
136
-
137
- ### Basic Usage
138
-
139
- **Step 1:** Create a `.env` file:
140
-
141
- ```env
142
- APP_PORT=8443
143
- APP_URL=http://localhost:${APP_PORT}
144
- DATABASE_URL=postgres://localhost:5432/mydb
145
- IS_PRODUCTION=false
146
- MAX_CONNECTIONS=100
147
- ALLOWED_ORIGINS=https://app.com,https://admin.com
148
- ```
149
-
150
- **JavaScript Example (Functional API):**
151
-
152
- ```js
153
- import { Envapter, Converters } from 'envapt';
154
-
155
- // Basic usage
156
- const port = Envapter.getNumber('APP_PORT', 3000);
157
- const url = Envapter.get('APP_URL', 'http://localhost:3000');
158
- const isProduction = Envapter.isProduction;
159
-
160
- console.log(`Server running on port ${port}`); // 8443
161
- console.log(`URL: ${url}`); // "http://localhost:8443"
162
-
163
- // Advanced converters
164
- const corsOrigins = Envapter.getUsing('ALLOWED_ORIGINS', Converters.Array, []);
165
- const dbConfig = Envapter.getUsing('DATABASE_CONFIG', Converters.Json, {});
166
-
167
- // Multi-key lookup: try primary, then replica, then fall back
168
- const dbUrl = Envapter.get(['PRIMARY_DB_URL', 'REPLICA_DB_URL'], 'sqlite://memory');
169
- const httpPort = Envapter.getNumber(['APP_PORT', 'PORT'], 3000);
170
-
171
- // Tagged template literals
172
- const message = Envapter.resolve`Server ${'APP_URL'} is ready!`;
173
- console.log(message); // "Server http://localhost:8443 is ready!"
174
- ```
175
-
176
- **TypeScript Example (Decorator API):**
23
+ `process.env` always hands you a `string | undefined`. envapt returns the type you asked for, with a
24
+ fallback that removes `undefined` from the return type.
177
25
 
178
26
  ```ts
179
- import { Envapt, Envapter, Converters } from 'envapt';
180
-
181
- // Global app configuration (static properties)
182
- class AppConfig extends Envapter {
183
- @Envapt('APP_PORT', 3000)
184
- static readonly port: number;
185
-
186
- // The Classic Syntax only works for Primitive Converters. Converters.Url is a Built-in Converter.
187
- @Envapt('APP_URL', { fallback: new URL('http://localhost:3000'), converter: Converters.Url })
188
- static readonly url: URL;
189
-
190
- // Prefer CLOUD_REDIS_URL but fall back to classic REDIS_URL when missing
191
- @Envapt(['CLOUD_REDIS_URL', 'REDIS_URL'], 'redis://localhost:6379')
192
- static readonly redisUrl: string;
193
-
194
- @Envapt('ALLOWED_ORIGINS', {
195
- fallback: ['http://localhost:3000'],
196
- converter: Converters.Array
197
- })
198
- static readonly allowedOrigins: string[];
199
- }
200
-
201
- // Service configuration (instance properties)
202
- class DatabaseService {
203
- @Envapt('DATABASE_URL', 'sqlite://memory')
204
- declare readonly databaseUrl: string;
205
-
206
- // Will detect that '10' is a number and set the runtime type accordingly
207
- @Envapt('MAX_CONNECTIONS', 10)
208
- declare readonly maxConnections: number;
209
-
210
- @Envapt('REQUEST_TIMEOUT', { converter: Converters.Time, fallback: 5000 })
211
- declare readonly timeout: number; // Converts "5s" to 5000ms
212
-
213
- async connect() {
214
- console.log(`Connecting to ${this.databaseUrl}`);
215
- // Connection logic here
216
- }
217
- }
218
-
219
- // Usage
220
- console.log(AppConfig.port); // 8443 (number)
221
- console.log(AppConfig.url.href); // "http://localhost:8443"
222
-
223
- const dbService = new DatabaseService();
224
- await dbService.connect();
225
- ```
226
-
227
- <div align="right">
228
-
229
- **[⬆️ Back to Top](#table-of-contents)**
230
-
231
- </div>
232
-
233
- ## API Reference
234
-
235
- ### Decorator API
236
-
237
- > [!IMPORTANT]
238
- > **TypeScript Only**: The `@Envapt` decorator API requires TypeScript with `experimentalDecorators: true`. JavaScript users should use the [Functional API](#functional-api) instead.
239
-
240
- The `@Envapt` decorator can be used on both **static** and **instance** class properties:
241
-
242
- - **Static properties**: Can use for global configuration that's shared across your entire application (e.g., app port, global features, environment settings)
243
- - **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)
244
-
245
- **Important**: Instance properties must be declared with `declare` keyword or `!` assertion since they're populated by the decorator rather than set in a constructor.
246
-
247
- #### Modern Syntax (Recommended)
248
-
249
- ```ts
250
- @Envapt('ENV_VAR', { fallback?: T, converter?: EnvConverter<T> })
251
- ```
252
-
253
- > [!NOTE]
254
- > The first argument can be a string **or** an ordered array of strings:
255
- > `@Envapt(['PRIMARY_URL', 'SECONDARY_URL'], { fallback: 'https://example.com' })`.
256
- > Envapt will resolve the first key that exists.
257
- >
258
- > [!TIP]
259
- > **Generic Typing for Better IntelliSense**
260
- >
261
- > You can specify explicit types using generics for better type safety and IntelliSense:
262
- >
263
- > ```ts
264
- > // Explicit typing provides better IntelliSense for complex types
265
- > @Envapt<DatabaseConfig>('DB_CONFIG', {
266
- > fallback: { host: 'localhost', port: 5432, ssl: false },
267
- > converter: Converters.Json
268
- > })
269
- > static readonly dbConfig: DatabaseConfig;
270
- > ```
271
-
272
- #### Classic Syntax
273
-
274
- ```ts
275
- @Envapt('ENV_VAR', fallback?, converter?)
276
- ```
277
-
278
- Need to chain multiple keys with the classic API? Pass an array instead of a string:
279
-
280
- ```ts
281
- @Envapt(['HOST_PRIMARY', 'HOST_SECONDARY'], 'localhost')
282
- static readonly host: string;
283
- ```
284
-
285
- #### Automatic Runtime Type Detection
286
-
287
- Types are automatically inferred from fallback values.
288
-
289
- ```ts
290
- class Config extends Envapter {
291
- // Static properties for global settings
292
- @Envapt('APP_NAME', 'MyApp') // string
293
- static readonly appName: string;
294
-
295
- @Envapt('APP_PORT', 3000) // number
296
- static readonly port: number;
297
-
298
- @Envapt('DEBUG_MODE', false) // boolean
299
- static readonly debugMode: boolean;
300
-
301
- // Instance properties for service-specific settings
302
- @Envapt('SMTP_HOST', 'localhost') // string
303
- declare readonly smtpHost: string;
304
-
305
- @Envapt('SMTP_PORT', 587) // number
306
- declare readonly smtpPort: number;
307
-
308
- @Envapt('SMTP_SECURE', true) // boolean
309
- declare readonly smtpSecure: boolean;
310
-
311
- sendEmail(to: string, subject: string) {
312
- console.log(`Sending via ${this.smtpHost}:${this.smtpPort}`);
313
- }
314
- }
315
- ```
316
-
317
- #### Primitive Converters
318
-
319
- Envapt allows using the 5 "primitive" type-like converters. These **will** coerce values.
320
-
321
- > [!NOTE]
322
- > The runtime validator will ignore this usage, allowing type coercion for flexibility.
323
-
324
- **Valid Primitive Types:** `String`, `Number`, `Boolean`, `Symbol`, and `BigInt`.
325
-
326
- ```ts
327
- class Config extends Envapter {
328
- @Envapt('PORT_STRING', { fallback: 'hello-world', converter: String })
329
- static readonly portAsString: string;
330
-
331
- @Envapt('DEBUG_FLAG', { fallback: true, converter: Boolean })
332
- static readonly debugMode: boolean;
333
-
334
- @Envapt('USER_ID', { fallback: 12345, converter: Number })
335
- static readonly userId: number;
336
-
337
- @Envapt('MAX_SAFE_INT', { fallback: 9007199254740991n, converter: BigInt })
338
- static readonly maxSafeInt: bigint;
339
-
340
- @Envapt('APP_INSTANCE', { fallback: Symbol(main), converter: Symbol })
341
- static readonly appInstance: symbol;
342
-
343
- // Instance properties work the same way
344
- @Envapt('CONNECTION_TIMEOUT', { fallback: 5000, converter: Number })
345
- declare readonly timeout: number;
27
+ import { Envapter } from 'envapt';
346
28
 
347
- // Type coercion example
348
- @Envapt('PERMISSIONS', { fallback: '72394823472342983', converter: BigInt })
349
- declare readonly permissions: bigint; // Converts "72394823472342983" to BigInt
350
- }
29
+ const port = Envapter.getNumber('PORT', 3000); // number, not string | undefined
351
30
  ```
352
31
 
353
- **When to use primitive converters:**
354
-
355
- - When you need explicit type coercion between incompatible types
356
- - When working with external systems that provide values in unexpected formats
357
-
358
- #### Built-in Converters
359
-
360
- Envapt provides many built-in converters for common patterns:
361
-
362
- > [!IMPORTANT]
363
- > **Use the `Converters` enum** instead of string literals. They look better, and provide better type inference:
364
- >
365
- > ```ts
366
- > import { Converters } from 'envapt';
367
- > // ✅ Recommended: Use enum
368
- > @Envapt('PORT', { converter: Converters.Number, fallback: 3000 })
369
- >
370
- > // ❌ Discouraged: String literals (still supported for compatibility)
371
- > @Envapt('PORT', { converter: 'number', fallback: 3000 })
372
- > ```
373
- >
374
- > 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.
375
-
376
- ```ts
377
- class Config extends Envapter {
378
- // Basic types
379
- @Envapt('APP_NAME', { converter: Converters.String, fallback: 'MyApp' })
380
- static readonly appName: string;
381
-
382
- @Envapt('PORT', { converter: Converters.Number, fallback: 3000 })
383
- static readonly port: number;
384
-
385
- @Envapt('PRODUCTION_MODE', { converter: Converters.Boolean, fallback: false })
386
- static readonly productionMode: boolean;
387
-
388
- // Advanced types
389
- @Envapt('CORS_ORIGINS', { converter: Converters.Array, fallback: [] })
390
- static readonly corsOrigins: string[];
32
+ **[Read the docs →](https://envapt.materwelon.dev)**
391
33
 
392
- @Envapt('CONFIG_JSON', { converter: Converters.Json, fallback: {} })
393
- static readonly config: object;
34
+ ## What you get
394
35
 
395
- @Envapt('API_URL', { converter: Converters.Url, fallback: new URL('http://localhost') })
396
- static readonly apiUrl: URL;
36
+ - **Typed values.** A fallback removes `undefined` from the return type. Built-in converters cover
37
+ numbers, booleans, bigint, JSON, URLs, regular expressions, dates, durations, and arrays, or pass
38
+ your own function or a Standard Schema validator (zod, valibot, arktype).
39
+ - **Zero runtime dependencies.** envapt ships its own `.env` parser, so nothing is added to your
40
+ dependency tree.
41
+ - **The same API on Node, Bun, and Deno.** Node `>=20`, Bun `>=1.3`, Deno `>=2.5`; ESM and CJS.
42
+ - **`.env` loading built in.** A per-environment file cascade, `${VAR}` templates, and strict /
43
+ required checks.
397
44
 
398
- @Envapt('TIMEOUT', { converter: Converters.Time, fallback: 5000 })
399
- static readonly timeout: number; // Converts "30s" to 30000ms
45
+ ## Install
400
46
 
401
- // Instance properties work the same way
402
- @Envapt('CACHE_TTL', { converter: Converters.Time, fallback: 3600000 })
403
- declare readonly cacheTtl: number; // "1h" becomes 3600000ms
404
- }
47
+ ```sh
48
+ npm install envapt
49
+ pnpm add envapt
50
+ yarn add envapt
51
+ bun add envapt
52
+ deno add jsr:@materwelon/envapt
405
53
  ```
406
54
 
407
- > [!WARNING]
408
- > These will throw runtime errors due to type mismatches:
409
- >
410
- > ```ts
411
- > // ❌ String converter with number fallback
412
- > @Envapt('VAR', { converter: Converters.String, fallback: 42 })
413
- >
414
- > // ❌ URL converter with string fallback
415
- > @Envapt('VAR', { converter: Converters.Url, fallback: 'http://example.com' })
416
- >
417
- > // ✅ Use primitive constructors for type coercion instead
418
- > @Envapt('VAR', { converter: String, fallback: 42 })
419
- > ```
420
-
421
- **Available Built-in Converters:**
55
+ ## Quick start
422
56
 
423
- | **Converter** | **Alias** | **Description** |
424
- | -------------------- | ----------- | -------------------------------------------------------------------- |
425
- | `Converters.String` | `'string'` | String values |
426
- | `Converters.Number` | `'number'` | Numeric values (integers and floats) |
427
- | `Converters.Integer` | `'integer'` | Integer values only |
428
- | `Converters.Float` | `'float'` | Float values only |
429
- | `Converters.Boolean` | `'boolean'` | Boolean values (`true`/`false`, `yes`/`no`, `on`/`off`, `1`/`0`) |
430
- | `Converters.Bigint` | `'bigint'` | BigInt values for large integers |
431
- | `Converters.Symbol` | `'symbol'` | Symbol values (creates symbols from string descriptions) |
432
- | `Converters.Json` | `'json'` | JSON objects/arrays (safe parsing with fallback) |
433
- | `Converters.Array` | `'array'` | Comma-separated string arrays |
434
- | `Converters.Url` | `'url'` | URL objects |
435
- | `Converters.Regexp` | `'regexp'` | Regular expressions (supports `/pattern/flags` syntax) |
436
- | `Converters.Date` | `'date'` | Date objects (supports ISO strings and timestamps) |
437
- | `Converters.Time` | `'time'` | Time values (e.g. `"5s"`, `"30m"`, `"2h"` converted to milliseconds) |
57
+ Read values functionally with `Envapter`, or bind them to class fields with the `@Envapt` decorator.
58
+ Both share the same parsing, converters, and cache.
438
59
 
439
- #### Custom Array Converters
60
+ ### Functional
440
61
 
441
- For more control over array parsing:
442
-
443
- > [!IMPORTANT]
444
- > Array converters validate that:
445
- >
446
- > 1. **Fallback must be an array** (if provided)
447
- > 2. **All fallback elements have consistent types** (no mixed types like `['string', 42, true]`)
448
- > 3. **Array converter `type` matches fallback element types** (if `type` is specified)
62
+ Read a value anywhere, in JavaScript or TypeScript. No build step.
449
63
 
450
64
  ```ts
451
- class Config extends Envapter {
452
- // Basic array (comma-separated strings)
453
- @Envapt('TAGS', { converter: Converters.Array, fallback: [] })
454
- static readonly tags: string[];
455
-
456
- // Custom delimiter
457
- @Envapt('ALLOWED_METHODS', { converter: { delimiter: '|' }, fallback: ['GET'] })
458
- declare readonly allowedMethods: string[];
459
-
460
- // Custom delimiter with type conversion
461
- @Envapt('RATE_LIMITS', { converter: { delimiter: ',', type: Converters.Number }, fallback: [100] })
462
- declare readonly rateLimits: number[];
463
-
464
- @Envapt('FEATURE_FLAGS', { converter: { delimiter: ';', type: 'boolean' }, fallback: [false] })
465
- declare readonly featureFlags: boolean[];
466
- }
467
- ```
468
-
469
- > [!WARNING]
470
- > These will throw runtime validation errors:
471
- >
472
- > ```ts
473
- > // ❌ Mixed types in fallback array
474
- > @Envapt('MIXED', { converter: Converters.Array, fallback: ['string', 42, true] })
475
- >
476
- > // ❌ Array converter type doesn't match fallback elements
477
- > @Envapt('NUMS', { converter: { delimiter: ',', type: Converters.Number }, fallback: ['not', 'numbers'] })
478
- >
479
- > // ❌ Non-array fallback with array converter
480
- > @Envapt('INVALID', { converter: Converters.Array, fallback: 'not-an-array' })
481
- > ```
482
-
483
- **ArrayConverter Interface:**
484
-
485
- - `delimiter: string` - The string used to split array elements
486
- - `type?: BuiltInConverter` - Optional type to convert each element to (excludes `Converters.Array`, `Converters.Json`, and `Converters.Regexp`)
487
-
488
- #### Custom Converters
489
-
490
- Transform environment values to any type:
491
-
492
- ```ts
493
- class Config extends Envapter {
494
- @Envapt('TAGS', {
495
- fallback: new Set(['default']),
496
- converter: (raw, fallback) => {
497
- if (!raw) return fallback;
498
- return new Set(raw.split(',').map((s) => s.trim()));
499
- }
500
- })
501
- static readonly tags: Set<string>;
502
-
503
- @Envapt('NOTIFICATION_CHANNELS', {
504
- fallback: new Map([['email', 'enabled']]),
505
- converter: (raw, fallback) => {
506
- if (!raw) return fallback;
507
- const map = new Map();
508
- raw.split(',').forEach((pair) => {
509
- const [key, value] = pair.split(':');
510
- map.set(key?.trim(), value?.trim() || 'enabled');
511
- });
512
- return map;
513
- }
514
- })
515
- declare readonly channels: Map<string, string>;
516
- }
517
- ```
518
-
519
- > [!TIP]
520
- > **Custom Validation with Error Throwing**
521
- >
522
- > Custom converters can throw errors for validation. The custom converter is called even when a variable is not found in the env file(s):
523
- >
524
- > ```ts
525
- > @Envapt<string>('API_KEY', {
526
- > converter(raw, _fallback) {
527
- > if (typeof raw !== 'string' || raw === '') {
528
- > throw new Error('API_KEY is required and cannot be empty');
529
- > }
530
- > return raw;
531
- > }
532
- > })
533
- > static readonly apiKey: string;
534
- > ```
535
- >
536
- > _No `fallback` needed here because the converter throws an error if a value is not what we want it to be_
537
-
538
- #### Handling Missing Values
539
-
540
- Control what happens when environment variables don't exist:
541
-
542
- ```ts
543
- class Config extends Envapter {
544
- // Returns undefined if not found
545
- @Envapt('OPTIONAL_FEATURE', { fallback: undefined })
546
- static readonly optionalFeature: string | undefined;
547
-
548
- // Returns null if not found (no fallback provided)
549
- @Envapt('MISSING_CONFIG', { converter: Converters.String })
550
- static readonly missingConfig: string | null;
551
-
552
- // Uses fallback if not found
553
- @Envapt('DEFAULT_THEME', { fallback: 'light' })
554
- static readonly defaultTheme: string;
555
-
556
- // Instance properties work the same way
557
- @Envapt('LOG_FILE_PATH', { fallback: undefined })
558
- declare readonly logFilePath: string | undefined;
559
- }
560
- ```
561
-
562
- ### Functional API
563
-
564
- For functional-style environment variable access on primitive types:
565
-
566
- ```js
567
- import { Envapter, Converters } from 'envapt';
568
-
569
- // Basic type-specific getters
570
- const str = Envapter.get('STRING_VAR', 'default');
571
- const num = Envapter.getNumber('NUMBER_VAR', 42);
572
- const bool = Envapter.getBoolean('BOOLEAN_VAR', false);
573
- const bigint = Envapter.getBigInt('BIGINT_VAR', 100n);
574
- const symbol = Envapter.getSymbol('SYMBOL_VAR', Symbol('default'));
575
-
576
- // Advanced converter methods
577
- const jsonData = Envapter.getUsing('CONFIG_JSON', Converters.Json);
578
- const urlArray = Envapter.getUsing('API_URLS', { delimiter: ',', type: Converters.Url });
579
- const customData = Envapter.getWith('RAW_DATA', (raw) => raw?.split('|').map((s) => s.trim()));
580
-
581
- // Multi-key inputs work everywhere: Envapt will read left-to-right
582
- const secretsHost = Envapter.get(['SECRETS_HOST', 'DEFAULT_HOST'], 'localhost');
583
-
584
- // Instance methods (same API available)
585
- const envapter = new Envapter();
586
- const value = envapter.get('VAR', 'default');
587
- const processed = envapter.getUsing('DATA', Converters.Array);
588
- ```
589
-
590
- For functional-style environment variable access with converters:
591
-
592
- ```js
593
65
  import { Envapter, Converters } from 'envapt';
594
66
 
595
- // Use built-in converters directly
596
- const config = Envapter.getUsing('API_CONFIG', Converters.Json, { default: 'value' });
597
- const urls = Envapter.getUsing('SERVICE_URLS', { delimiter: '|', type: Converters.Url });
598
- const pgUrl = Envapter.getUsing(['PRIMARY_PG_URL', 'SECONDARY_PG_URL'], Converters.Url);
599
-
600
- // TypeScript: Use type override for better type inference
601
- const typedConfig = Envapter.getUsing<{ host: string; port: number; ssl: boolean }>('DATABASE_CONFIG', Converters.Json);
602
- // typedConfig is now typed as { host: string; port: number; ssl: boolean } instead of JsonValue | undefined
603
-
604
- // Use custom converter functions
605
- const processedData = Envapter.getWith(
606
- 'RAW_DATA',
607
- (raw, fallback) => {
608
- if (!raw) return fallback ?? [];
609
- return raw.split(',').map((item) => ({ name: item.trim(), enabled: true }));
610
- },
611
- []
612
- );
613
-
614
- // Instance methods work the same way
615
- const envapter = new Envapter();
616
- const result = envapter.getUsing('DATABASE_CONFIG', Converters.Json);
617
- ```
618
-
619
- > [!TIP]
620
- > **Type Override with `getUsing`**
621
- >
622
- > You can explicitly specify the return type for `getUsing` when TypeScript's inference isn't specific enough (especially useful with `Converters.Json`):
623
- >
624
- > ```ts
625
- > // Default behavior
626
- > const config = Envapter.getUsing('CONFIG', Converters.Json); // type: JsonValue | undefined (undefined because no fallback)
627
- >
628
- > // Override with specific interface
629
- > interface DatabaseConfig {
630
- > host: string;
631
- > port: number;
632
- > ssl: boolean;
633
- > }
634
- > const dbConfig = Envapter.getUsing<DatabaseConfig>('DB_CONFIG', Converters.Json);
635
- > // dbConfig is now properly typed as DatabaseConfig
636
- > ```
637
- >
638
- > _Make sure the fallback value matches the expected type, if you use a fallback. Otherwise you'll see a TypeScript error._\
639
- > _This does NOT validate the type at runtime. You'll need to handle that yourself._
640
-
641
- ### Converter Type Quick Reference
642
-
643
- | **Use Case** | **Converter Type** | **Example** |
644
- | ----------------------- | ------------------------- | --------------------------------------------------------- |
645
- | **Type coercion** | Primitive constructors | `converter: String` |
646
- | **Strict validation** | Built-in converters | `converter: Converters.String` |
647
- | **Array parsing** | Built-in Array converters | `converter: { delimiter: ',', type?: Converters.String }` |
648
- | **Complex transforms** | Custom function | `converter: (raw, fallback) => ...` |
649
- | **Functional built-in** | `getUsing()` method | `Envapter.getUsing('VAR', Converters.Json)` |
650
- | **Type override** | `getUsing<T>()` method | `Envapter.getUsing<MyType>('VAR', Converters.Json)` |
651
- | **Functional custom** | `getWith()` method | `Envapter.getWith('VAR', (raw) => transform(raw))` |
652
-
653
- > [!TIP]
654
- > **Use the `Converters` enum**. They look better. Start with built-in converters, use primitive constructors when you need coercion, and custom converters for complex transforms.
655
-
656
- ### Tagged Template Resolver
657
-
658
- Envapt provides a convenient tagged template literal syntax for resolving environment variables directly in template strings:
659
-
660
- ```js
661
- import { Envapter } from 'envapt';
662
-
663
- // Given these environment variables:
664
- // API_HOST=api.example.com
665
- // API_PORT=8080
666
- // API_URL=https://${API_HOST}:${API_PORT}
667
- // SERVICE_NAME=UserService
668
-
669
- // Use tagged template literals for string interpolation
670
- const endpoint = Envapter.resolve`Connecting to ${'SERVICE_NAME'} at ${'API_URL'}`;
671
- // Returns: "Connecting to UserService at https://api.example.com:8080"
672
-
673
- const logMessage = Envapter.resolve`Starting ${'SERVICE_NAME'} on port ${'API_PORT'}`;
674
- // Returns: "Starting UserService on port 8080"
675
-
676
- // Works with instance methods too
677
- const envapter = new Envapter();
678
- const status = envapter.resolve`${'SERVICE_NAME'} is running`;
679
- // Returns: "UserService is running"
67
+ const port = Envapter.getNumber('PORT', 3000);
68
+ const origins = Envapter.getUsing('ALLOWED_ORIGINS', Converters.array(), []);
680
69
  ```
681
70
 
682
- Works seamlessly with template variables in your `.env` file:
71
+ ### Decorator
683
72
 
684
- ```env
685
- # Your .env file
686
- API_HOST=api.example.com
687
- API_PORT=8080
688
- API_URL=https://${API_HOST}:${API_PORT} # Template resolved first
689
- SERVICE_NAME=UserService
690
- ```
73
+ Bind a value to a class field. TypeScript, with `experimentalDecorators` in your `tsconfig.json`.
691
74
 
692
75
  ```ts
693
- const message = Envapter.resolve`Service ${'SERVICE_NAME'} endpoint: ${'API_URL'}`;
694
- // Returns: "Service UserService endpoint: https://api.example.com:8080"
695
- ```
696
-
697
- > [!NOTE]
698
- > Tagged template literals work with any environment variables, including those that use `${VAR}` template syntax in your `.env` file. The template resolution happens first, then the tagged template interpolation.
699
-
700
- <div align="right">
701
-
702
- **[⬆️ Back to Top](#table-of-contents)**
703
-
704
- </div>
705
-
706
- ## Environment Detection
707
-
708
- Envapt automatically detects your environment from these variables (in order):
709
-
710
- 1. `ENVIRONMENT`
711
- 2. `ENV`
712
- 3. `NODE_ENV`
713
-
714
- Supported values: `development`, `staging`, `production` (case-sensitive)
715
-
716
- ### Environment Management
717
-
718
- ```js
719
- import { Envapter, EnvaptEnvironment } from 'envapt';
720
-
721
- // Check current environment
722
- console.log(Envapter.environment); // Environment.Development (default)
723
- console.log(Envapter.isProduction); // false
724
- console.log(Envapter.isDevelopment); // true
725
- console.log(Envapter.isStaging); // false
726
-
727
- // Set environment
728
- Envapter.environment = EnvaptEnvironment.Production;
729
- Envapter.environment = 'staging'; // string also works
730
- ```
731
-
732
- <div align="right">
733
-
734
- **[⬆️ Back to Top](#table-of-contents)**
735
-
736
- </div>
737
-
738
- ## Configuration
739
-
740
- ### Multiple .env Files
741
-
742
- ```js
743
- import { resolve } from 'node:path';
744
- import { Envapter } from 'envapt';
745
-
746
- // Load from multiple files
747
- Envapter.envPaths = [resolve(import.meta.dirname, '.env.local'), resolve(import.meta.dirname, '.env.production')];
748
-
749
- // Or single file
750
- Envapter.envPaths = resolve(import.meta.dirname, '.env.production');
751
-
752
- // Or just don't set a path for it to default to .env at the root of your project
753
-
754
- // Also, in CommonJS, use `__dirname` instead of `import.meta.dirname`:
755
- ```
756
-
757
- ### Dotenv Configuration
758
-
759
- Envapt allows you to customize dotenv behavior by setting configuration options:
760
-
761
- ```js
762
- import { Envapter } from 'envapt';
763
-
764
- // Set dotenv configuration options
765
- Envapter.dotenvConfig = {
766
- encoding: 'latin1', // File encoding (default: 'utf8')
767
- debug: true, // Enable debug logging
768
- override: true, // Override existing environment variables
769
- quiet: false, // Suppress non-error output (default: true)
770
- DOTENV_KEY: 'key...' // Decryption key for .env.vault files
771
- };
772
-
773
- // Get current configuration
774
- console.log(Envapter.dotenvConfig);
775
- ```
776
-
777
- > [!NOTE]
778
- > The `path` and `processEnv` options are managed internally by Envapter and cannot be set via `dotenvConfig`.
779
-
780
- <div align="right">
781
-
782
- **[⬆️ Back to Top](#table-of-contents)**
783
-
784
- </div>
785
-
786
- ## Template Variables
787
-
788
- Envapt supports variable interpolation with `${VARIABLE}` syntax:
789
-
790
- ```env
791
- DATABASE_HOST=localhost
792
- DATABASE_PORT=5432
793
- DATABASE_URL=postgres://${DATABASE_HOST}:${DATABASE_PORT}/mydb
794
-
795
- API_VERSION=v1
796
- API_BASE=https://api.example.com
797
- API_ENDPOINT=${API_BASE}/${API_VERSION}/users
798
- ```
799
-
800
- ### Circular Reference Protection
801
-
802
- ```env
803
- CIRCULAR_A=${CIRCULAR_B}
804
- CIRCULAR_B=${CIRCULAR_A}
805
- ```
806
-
807
- Circular references are detected and preserved as-is rather than causing infinite loops.
808
-
809
- <div align="right">
810
-
811
- **[⬆️ Back to Top](#table-of-contents)**
812
-
813
- </div>
814
-
815
- ## Error Handling
816
-
817
- Envapt provides detailed error codes for better debugging and error handling:
818
-
819
- ```js
820
- import { EnvaptError, EnvaptErrorCodes } from 'envapt';
76
+ import { Envapt, Converters } from 'envapt';
821
77
 
822
- try {
823
- // This will throw an error for invalid configuration
824
- Envapter.dotenvConfig = { path: '.env.custom' };
825
- } catch (error) {
826
- if (error instanceof EnvaptError) {
827
- console.log('Error code:', error.code);
828
- console.log('Error message:', error.message);
829
-
830
- // Handle specific error types
831
- switch (error.code) {
832
- case EnvaptErrorCodes.InvalidUserDefinedConfig:
833
- console.log('Invalid configuration provided');
834
- break;
835
- case EnvaptErrorCodes.EnvFileNotFound:
836
- console.log('Environment file not found');
837
- break;
838
- default:
839
- console.warn('Unhandled error code:', error.code);
840
- break;
841
- }
842
- }
843
- }
844
- ```
845
-
846
- ### Error Code Reference
847
-
848
- #### 🔧 Fallback Errors (1xx)
849
-
850
- | **Error Code** | **Description** |
851
- | ---------------------------------------- | --------------------------------------------------------- |
852
- | `InvalidFallback` (101) | Invalid fallback value provided |
853
- | `InvalidFallbackType` (102) | Fallback value type doesn't match expected converter type |
854
- | `ArrayFallbackElementTypeMismatch` (103) | Array fallback contains elements of wrong type |
855
- | `FallbackConverterTypeMismatch` (104) | Fallback type doesn't match the specified converter |
856
-
857
- #### 🧪 Converter Errors (2xx)
858
-
859
- | **Error Code** | **Description** |
860
- | --------------------------------- | ---------------------------------------------- |
861
- | `InvalidArrayConverterType` (201) | Invalid array converter configuration provided |
862
- | `InvalidBuiltInConverter` (202) | Invalid built-in converter specified |
863
- | `InvalidCustomConverter` (203) | Custom converter function is invalid |
864
- | `InvalidConverterType` (204) | Converter type is not recognized |
865
- | `PrimitiveCoercionFailed` (205) | Primitive type coercion failed |
866
-
867
- #### 📂 Environment File & Config Errors (3xx)
868
-
869
- | **Error Code** | **Description** |
870
- | -------------------------------- | ---------------------------------------------- |
871
- | `MissingDelimiter` (301) | Delimiter is missing in array converter config |
872
- | `InvalidUserDefinedConfig` (302) | Invalid user-defined configuration provided |
873
- | `EnvFilesNotFound` (303) | Specified environment file doesn't exist |
874
- | `InvalidKeyInput` (304) | Invalid key input (not string or string array) |
875
-
876
- <div align="right">
877
-
878
- **[⬆️ Back to Top](#table-of-contents)**
879
-
880
- </div>
881
-
882
- ## Advanced Examples
883
-
884
- ### JavaScript
885
-
886
- ```js
887
- import { Envapter, Converters } from 'envapt';
888
-
889
- // Global configuration
890
- const config = {
891
- port: Envapter.getNumber('PORT', 3000),
892
- requestTimeout: Envapter.getUsing('REQUEST_TIMEOUT', Converters.Time, 10000), // "5s" -> 5000ms
893
- featureFlags: Envapter.getWith(
894
- 'FEATURE_FLAGS',
895
- (raw, fallback) => {
896
- if (!raw) return fallback;
897
- return new Set(raw.split(',').map((s) => s.trim()));
898
- },
899
- new Set(['basic'])
900
- )
901
- };
902
-
903
- // Service configuration
904
- class DatabaseService {
905
- constructor() {
906
- this.databaseUrl = Envapter.get('DB_URL', 'sqlite://memory');
907
- this.cacheTtl = Envapter.getUsing('CACHE_TTL', Converters.Time, 3600000); // "1h" -> 3600000ms
908
- this.redisUrls = Envapter.getWith(
909
- 'REDIS_URLS',
910
- (raw, fallback) => (raw ? raw.split(',').map((s) => new URL(s)) : fallback),
911
- [new URL('redis://localhost:6379')]
912
- );
913
- }
914
-
915
- async initialize() {
916
- console.log(`App running on port ${config.port}`);
917
- console.log(`Database: ${this.databaseUrl}`);
918
- console.log(`Cache TTL: ${this.cacheTtl}ms`);
919
- }
78
+ class Config {
79
+ @Envapt('PORT', { converter: Converters.Number, fallback: 3000 })
80
+ declare static readonly port: number;
920
81
  }
921
82
  ```
922
83
 
923
- ### TypeScript
924
-
925
- ```ts
926
- import { Envapt, Envapter, Converters } from 'envapt';
927
-
928
- class AppConfig extends Envapter {
929
- // Global settings (static)
930
- @Envapt('PORT', 3000)
931
- static readonly port: number;
84
+ ## Documentation
932
85
 
933
- @Envapt('REQUEST_TIMEOUT', { converter: Converters.Time, fallback: 10000 })
934
- static readonly requestTimeout: number; // "5s" -> 5000ms (if env is set to "5s")
86
+ The guide, converter reference, validation, configuration, and the v4 to v5 migration live at
87
+ **[envapt.materwelon.dev](https://envapt.materwelon.dev)**.
935
88
 
936
- @Envapt('FEATURE_FLAGS', {
937
- fallback: new Set(['basic']),
938
- converter: (raw, fallback) => {
939
- if (!raw) return fallback;
940
- return new Set(raw.split(',').map((s) => s.trim()));
941
- }
942
- })
943
- static readonly featureFlags: Set<string>;
89
+ ## Agent skill
944
90
 
945
- // Service settings (instance)
946
- @Envapt('DB_URL', 'sqlite://memory')
947
- declare readonly databaseUrl: string;
91
+ Install the envapt agent skill so AI coding tools use the correct API:
948
92
 
949
- @Envapt('CACHE_TTL', { converter: Converters.Time, fallback: 3600000 })
950
- declare readonly cacheTtl: number; // "1h" -> 3600000ms
951
-
952
- @Envapt('REDIS_URLS', {
953
- fallback: [new URL('redis://localhost:6379')],
954
- converter: (raw, fallback) => (raw ? raw.split(',').map((s) => new URL(s)) : fallback)
955
- })
956
- declare readonly redisUrls: URL[];
957
-
958
- async initialize() {
959
- console.log(`App running on port ${AppConfig.port}`);
960
- console.log(`Database: ${this.databaseUrl}`);
961
- console.log(`Cache TTL: ${this.cacheTtl}ms`);
962
- }
963
- }
93
+ ```sh
94
+ npx skills add materwelonDhruv/envapt
964
95
  ```
965
96
 
966
- <div align="right">
967
-
968
- **[⬆️ Back to Top](#table-of-contents)**
969
-
970
- </div>
971
-
972
97
  ---
973
98
 
974
- <hr/>
975
-
976
- <p align="center">
977
- <a href="https://github.com/materwelondhruv/envapt">⭐️ Star it on GitHub</a> •
978
- <a href="https://github.com/materwelondhruv/envapt/issues">🐛 Report a bug</a> •
979
- <a href="https://github.com/materwelondhruv/envapt/issues/new?labels=enhancement">💡 Request a feature</a>
980
- </p>
981
-
982
- <p align="center">
983
- <sub>
984
- Built by <a href="https://github.com/materwelondhruv">@materwelonDhruv</a> • Licensed under
985
- <a href="LICENSE">Apache 2.0</a>
986
- </sub>
987
- </p>
99
+ <p align="center"><sub>Built by <a href="https://github.com/materwelondhruv">@materwelonDhruv</a> · Apache 2.0</sub></p>