env-validated 1.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/LICENSE +21 -0
- package/README.md +833 -0
- package/dist/adapters/arktype.cjs +38 -0
- package/dist/adapters/arktype.d.cts +1 -0
- package/dist/adapters/arktype.d.ts +1 -0
- package/dist/adapters/arktype.js +11 -0
- package/dist/adapters/effect.cjs +38 -0
- package/dist/adapters/effect.d.cts +1 -0
- package/dist/adapters/effect.d.ts +1 -0
- package/dist/adapters/effect.js +11 -0
- package/dist/adapters/joi.cjs +57 -0
- package/dist/adapters/joi.d.cts +55 -0
- package/dist/adapters/joi.d.ts +55 -0
- package/dist/adapters/joi.js +35 -0
- package/dist/adapters/runtypes.cjs +38 -0
- package/dist/adapters/runtypes.d.cts +1 -0
- package/dist/adapters/runtypes.d.ts +1 -0
- package/dist/adapters/runtypes.js +11 -0
- package/dist/adapters/superstruct.cjs +38 -0
- package/dist/adapters/superstruct.d.cts +1 -0
- package/dist/adapters/superstruct.d.ts +1 -0
- package/dist/adapters/superstruct.js +11 -0
- package/dist/adapters/typebox.cjs +38 -0
- package/dist/adapters/typebox.d.cts +1 -0
- package/dist/adapters/typebox.d.ts +1 -0
- package/dist/adapters/typebox.js +11 -0
- package/dist/adapters/valibot.cjs +38 -0
- package/dist/adapters/valibot.d.cts +1 -0
- package/dist/adapters/valibot.d.ts +1 -0
- package/dist/adapters/valibot.js +11 -0
- package/dist/adapters/yup.cjs +38 -0
- package/dist/adapters/yup.d.cts +1 -0
- package/dist/adapters/yup.d.ts +1 -0
- package/dist/adapters/yup.js +11 -0
- package/dist/adapters/zod.cjs +38 -0
- package/dist/adapters/zod.d.cts +1 -0
- package/dist/adapters/zod.d.ts +1 -0
- package/dist/adapters/zod.js +11 -0
- package/dist/effect-DIhhk_ck.d.cts +222 -0
- package/dist/effect-DIhhk_ck.d.ts +222 -0
- package/dist/index.cjs +737 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +714 -0
- package/package.json +215 -0
package/README.md
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
# env-validated
|
|
2
|
+
|
|
3
|
+
**Framework-agnostic environment variable validation with pluggable validators, zero dependencies, and full TypeScript inference.**
|
|
4
|
+
|
|
5
|
+
Validate your env vars at startup. Get a fully typed, frozen config object. Never touch `process.env` directly again.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/env-validated)
|
|
8
|
+
[](https://github.com/husainkorasawala/env-validated/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why env-validated?
|
|
13
|
+
|
|
14
|
+
Every project solves environment validation from scratch. Existing solutions are either locked to a specific validator (t3-env requires Zod), lack TypeScript inference (envalid), or can't work across different runtimes.
|
|
15
|
+
|
|
16
|
+
env-validated fills all three gaps at once:
|
|
17
|
+
|
|
18
|
+
- **Zero dependencies** — the core package has no runtime dependencies
|
|
19
|
+
- **Full TypeScript inference** — no type casting, no manual type declarations
|
|
20
|
+
- **Pluggable validators** — use Zod, Joi, Yup, Valibot, or 5 other libraries via auto-detected adapters
|
|
21
|
+
- **Object schemas** — pass a pre-defined `z.object()`, `yup.object()`, etc. directly
|
|
22
|
+
- **Any runtime** — Node.js, Vite, Next.js, Deno, Cloudflare Workers, CLI tools
|
|
23
|
+
- **Mix and match** — use different validators per field in the same schema
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install env-validated
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createEnv } from 'env-validated'
|
|
35
|
+
|
|
36
|
+
export const env = createEnv({
|
|
37
|
+
schema: {
|
|
38
|
+
API_URL: { type: 'url', required: true },
|
|
39
|
+
PORT: { type: 'number', default: 3000 },
|
|
40
|
+
NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'] as const },
|
|
41
|
+
ENABLE_FEATURE: { type: 'boolean', default: false },
|
|
42
|
+
SECRET_KEY: { type: 'string', required: true, minLength: 32 },
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Fully typed - no casting needed:
|
|
47
|
+
env.PORT // number
|
|
48
|
+
env.ENABLE_FEATURE // boolean
|
|
49
|
+
env.NODE_ENV // 'development' | 'production' | 'test'
|
|
50
|
+
env.API_URL // string
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If any variable is missing or invalid, env-validated throws a single clear error listing every problem:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
[env-validated] Missing or invalid environment variables:
|
|
57
|
+
✖ API_URL — required, was not set
|
|
58
|
+
✖ SECRET_KEY — must be at least 32 characters (got 12)
|
|
59
|
+
✖ NODE_ENV — must be one of: development, production, test (got "staging")
|
|
60
|
+
✖ PORT — must be a number (got "not-a-number")
|
|
61
|
+
|
|
62
|
+
Fix these before starting the app.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Built-in Types
|
|
66
|
+
|
|
67
|
+
env-validated ships with lightweight validators for the most common types:
|
|
68
|
+
|
|
69
|
+
| Type | Output Type | Options | Description |
|
|
70
|
+
|------|------------|---------|-------------|
|
|
71
|
+
| `string` | `string` | `required`, `minLength`, `maxLength`, `pattern` | Plain string validation |
|
|
72
|
+
| `number` | `number` | `required`, `min`, `max`, `default` | Coerces string to number |
|
|
73
|
+
| `boolean` | `boolean` | `required`, `default` | Parses `true`/`false`/`1`/`0`/`yes`/`no` |
|
|
74
|
+
| `url` | `string` | `required`, `default` | Validates URL format |
|
|
75
|
+
| `enum` | union type | `values`, `required`, `default` | Must match one of `values[]` |
|
|
76
|
+
| `json` | `unknown` | `required`, `default` | Parses and validates JSON strings |
|
|
77
|
+
| `port` | `number` | `required`, `default` | Validates port range 1–65535 |
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const env = createEnv({
|
|
81
|
+
schema: {
|
|
82
|
+
DB_HOST: { type: 'string', default: 'localhost' },
|
|
83
|
+
DB_PORT: { type: 'port', default: 5432 },
|
|
84
|
+
LOG_JSON: { type: 'boolean', default: false },
|
|
85
|
+
REGION: { type: 'enum', values: ['us-east', 'eu-west'] as const },
|
|
86
|
+
METADATA: { type: 'json' },
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Pluggable Validators
|
|
92
|
+
|
|
93
|
+
env-validated auto-detects which validator library a schema field belongs to. No adapter imports or configuration needed — just pass the schema object directly.
|
|
94
|
+
|
|
95
|
+
### Zod
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm install zod
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createEnv } from 'env-validated'
|
|
103
|
+
import { z } from 'zod'
|
|
104
|
+
|
|
105
|
+
const env = createEnv({
|
|
106
|
+
schema: {
|
|
107
|
+
PORT: z.coerce.number().min(1000),
|
|
108
|
+
TAGS: z.string().transform(s => s.split(',')),
|
|
109
|
+
ENV: z.enum(['dev', 'prod', 'test']),
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
env.PORT // number
|
|
114
|
+
env.TAGS // string[]
|
|
115
|
+
env.ENV // 'dev' | 'prod' | 'test'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Joi
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install joi
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { createEnv } from 'env-validated'
|
|
126
|
+
import Joi from 'joi'
|
|
127
|
+
|
|
128
|
+
const env = createEnv({
|
|
129
|
+
schema: {
|
|
130
|
+
PORT: Joi.number().min(1000).required(),
|
|
131
|
+
NAME: Joi.string().min(3).required(),
|
|
132
|
+
ENV: Joi.string().valid('dev', 'prod', 'test').required(),
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
env.PORT // number
|
|
137
|
+
env.NAME // string
|
|
138
|
+
env.ENV // string
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Joi's type system carries schema types through structural matching, so `Joi.string()` infers `string`, `Joi.number()` infers `number`, and `Joi.boolean()` infers `boolean` automatically.
|
|
142
|
+
|
|
143
|
+
### Yup
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm install yup
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { createEnv } from 'env-validated'
|
|
151
|
+
import * as yup from 'yup'
|
|
152
|
+
|
|
153
|
+
const env = createEnv({
|
|
154
|
+
schema: {
|
|
155
|
+
PORT: yup.number().required().min(1000),
|
|
156
|
+
NAME: yup.string().required().min(3),
|
|
157
|
+
FLAG: yup.boolean().required(),
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
env.PORT // number
|
|
162
|
+
env.FLAG // boolean
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The Yup adapter pre-coerces string values to `number` or `boolean` based on the schema type, so you don't need `.transform()`.
|
|
166
|
+
|
|
167
|
+
### Valibot
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm install valibot
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { createEnv } from 'env-validated'
|
|
175
|
+
import * as v from 'valibot'
|
|
176
|
+
|
|
177
|
+
const env = createEnv({
|
|
178
|
+
schema: {
|
|
179
|
+
NAME: v.pipe(v.string(), v.minLength(3)),
|
|
180
|
+
ENV: v.picklist(['dev', 'prod', 'test']),
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
env.NAME // string
|
|
185
|
+
env.ENV // 'dev' | 'prod' | 'test'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### TypeBox
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm install @sinclair/typebox
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { createEnv } from 'env-validated'
|
|
196
|
+
import { Type } from '@sinclair/typebox'
|
|
197
|
+
|
|
198
|
+
const env = createEnv({
|
|
199
|
+
schema: {
|
|
200
|
+
PORT: Type.Number(),
|
|
201
|
+
NAME: Type.String({ minLength: 3 }),
|
|
202
|
+
FLAG: Type.Boolean(),
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
env.PORT // number
|
|
207
|
+
env.FLAG // boolean
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
TypeBox values are automatically coerced from strings using `Value.Convert`.
|
|
211
|
+
|
|
212
|
+
### ArkType
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npm install arktype
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { createEnv } from 'env-validated'
|
|
220
|
+
import { type } from 'arktype'
|
|
221
|
+
|
|
222
|
+
const env = createEnv({
|
|
223
|
+
schema: {
|
|
224
|
+
NAME: type('string >= 3'),
|
|
225
|
+
ENV: type("'dev' | 'prod' | 'test'"),
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Superstruct
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm install superstruct
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { createEnv } from 'env-validated'
|
|
238
|
+
import { string, size, enums } from 'superstruct'
|
|
239
|
+
|
|
240
|
+
const env = createEnv({
|
|
241
|
+
schema: {
|
|
242
|
+
NAME: size(string(), 3, 100),
|
|
243
|
+
ENV: enums(['dev', 'prod', 'test']),
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Runtypes
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npm install runtypes
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { createEnv } from 'env-validated'
|
|
256
|
+
import { String, Union, Literal } from 'runtypes'
|
|
257
|
+
|
|
258
|
+
const env = createEnv({
|
|
259
|
+
schema: {
|
|
260
|
+
NAME: String.withConstraint(s => s.length >= 3 || 'too short'),
|
|
261
|
+
ENV: Union(Literal('dev'), Literal('prod'), Literal('test')),
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Effect Schema
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npm install effect
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
For older setups that only use the standalone package, install `@effect/schema` instead; env-validated supports both as optional peers.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { createEnv } from 'env-validated'
|
|
276
|
+
import { Schema } from 'effect'
|
|
277
|
+
|
|
278
|
+
const env = createEnv({
|
|
279
|
+
schema: {
|
|
280
|
+
NAME: Schema.String.pipe(Schema.minLength(3)),
|
|
281
|
+
ENV: Schema.Literal('dev', 'prod', 'test'),
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Object-Level Schemas
|
|
287
|
+
|
|
288
|
+
If you already have a pre-defined object schema from any validator library, you can pass it directly to `createEnv` instead of defining fields individually. Types are inferred automatically.
|
|
289
|
+
|
|
290
|
+
### Zod
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { z } from 'zod'
|
|
294
|
+
|
|
295
|
+
const envSchema = z.object({
|
|
296
|
+
HOST: z.string(),
|
|
297
|
+
PORT: z.coerce.number(),
|
|
298
|
+
DEBUG: z.coerce.boolean(),
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const env = createEnv({ schema: envSchema })
|
|
302
|
+
|
|
303
|
+
env.HOST // string
|
|
304
|
+
env.PORT // number
|
|
305
|
+
env.DEBUG // boolean
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Yup
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import * as yup from 'yup'
|
|
312
|
+
|
|
313
|
+
const envSchema = yup.object({
|
|
314
|
+
HOST: yup.string().required(),
|
|
315
|
+
PORT: yup.number().required(),
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const env = createEnv({ schema: envSchema })
|
|
319
|
+
|
|
320
|
+
env.HOST // string
|
|
321
|
+
env.PORT // number
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Valibot
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import * as v from 'valibot'
|
|
328
|
+
|
|
329
|
+
const envSchema = v.object({
|
|
330
|
+
HOST: v.string(),
|
|
331
|
+
PORT: v.string(),
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const env = createEnv({ schema: envSchema })
|
|
335
|
+
|
|
336
|
+
env.HOST // string
|
|
337
|
+
env.PORT // string
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### TypeBox
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { Type } from '@sinclair/typebox'
|
|
344
|
+
|
|
345
|
+
const envSchema = Type.Object({
|
|
346
|
+
HOST: Type.String(),
|
|
347
|
+
PORT: Type.Number(),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const env = createEnv({ schema: envSchema })
|
|
351
|
+
|
|
352
|
+
env.HOST // string
|
|
353
|
+
env.PORT // number
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### ArkType
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { type } from 'arktype'
|
|
360
|
+
|
|
361
|
+
const envSchema = type({
|
|
362
|
+
HOST: 'string',
|
|
363
|
+
PORT: 'string',
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const env = createEnv({ schema: envSchema })
|
|
367
|
+
|
|
368
|
+
env.HOST // string
|
|
369
|
+
env.PORT // string
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Superstruct
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { object, string } from 'superstruct'
|
|
376
|
+
|
|
377
|
+
const envSchema = object({
|
|
378
|
+
HOST: string(),
|
|
379
|
+
PORT: string(),
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const env = createEnv({ schema: envSchema })
|
|
383
|
+
|
|
384
|
+
env.HOST // string
|
|
385
|
+
env.PORT // string
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Runtypes
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { Object, String } from 'runtypes'
|
|
392
|
+
|
|
393
|
+
const envSchema = Object({
|
|
394
|
+
HOST: String,
|
|
395
|
+
PORT: String,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
const env = createEnv({ schema: envSchema })
|
|
399
|
+
|
|
400
|
+
env.HOST // string
|
|
401
|
+
env.PORT // string
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Effect Schema
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { Schema } from 'effect'
|
|
408
|
+
|
|
409
|
+
const envSchema = Schema.Struct({
|
|
410
|
+
HOST: Schema.String,
|
|
411
|
+
PORT: Schema.String,
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
const env = createEnv({ schema: envSchema })
|
|
415
|
+
|
|
416
|
+
env.HOST // string
|
|
417
|
+
env.PORT // string
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Joi
|
|
421
|
+
|
|
422
|
+
Joi's type definitions don't preserve inner schema types in `Joi.object()`. Use the `joiObject()` wrapper for full type inference — no manual annotations needed:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import Joi from 'joi'
|
|
426
|
+
import { joiObject } from 'env-validated/adapters/joi'
|
|
427
|
+
|
|
428
|
+
const env = createEnv({
|
|
429
|
+
schema: joiObject({
|
|
430
|
+
HOST: Joi.string().required(),
|
|
431
|
+
PORT: Joi.number().min(1000).required(),
|
|
432
|
+
DEBUG: Joi.boolean().default(false),
|
|
433
|
+
}),
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
env.HOST // string
|
|
437
|
+
env.PORT // number
|
|
438
|
+
env.DEBUG // boolean
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
> **Why the wrapper?** `Joi.object<TSchema = any>()` erases inner schema types at the TypeScript level — this is a Joi limitation. `joiObject()` preserves them by attaching a phantom type that the inference engine reads. No manual type annotations are required; it infers `string`, `number`, `boolean`, and `Date` from the Joi schema methods automatically.
|
|
442
|
+
|
|
443
|
+
## Mixing Validators
|
|
444
|
+
|
|
445
|
+
You can use different validators for different fields in the same schema:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import { createEnv } from 'env-validated'
|
|
449
|
+
import { z } from 'zod'
|
|
450
|
+
import Joi from 'joi'
|
|
451
|
+
|
|
452
|
+
const env = createEnv({
|
|
453
|
+
schema: {
|
|
454
|
+
// Zod
|
|
455
|
+
PORT: z.coerce.number().min(1000),
|
|
456
|
+
|
|
457
|
+
// Joi
|
|
458
|
+
API_KEY: Joi.string().min(32).required(),
|
|
459
|
+
|
|
460
|
+
// Built-in
|
|
461
|
+
NODE_ENV: { type: 'enum', values: ['dev', 'prod'] as const },
|
|
462
|
+
|
|
463
|
+
// Custom validate function
|
|
464
|
+
ALLOWED_IPS: {
|
|
465
|
+
validate: (val) => {
|
|
466
|
+
if (!val) return { success: false, error: 'required' }
|
|
467
|
+
const ips = val.split(',').map(s => s.trim())
|
|
468
|
+
const valid = ips.every(ip => /^\d{1,3}(\.\d{1,3}){3}$/.test(ip))
|
|
469
|
+
return valid
|
|
470
|
+
? { success: true, value: ips }
|
|
471
|
+
: { success: false, error: 'Must be comma-separated IPs' }
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Custom Validate Function
|
|
479
|
+
|
|
480
|
+
For one-off fields that don't need a full library, pass an object with a `validate` function:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
const env = createEnv({
|
|
484
|
+
schema: {
|
|
485
|
+
CSV_LIST: {
|
|
486
|
+
validate: (val) => {
|
|
487
|
+
if (!val) return { success: false, error: 'required' }
|
|
488
|
+
const items = val.split(',').map(s => s.trim())
|
|
489
|
+
return { success: true, value: items }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
// env.CSV_LIST is inferred as string[]
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The `validate` function receives the raw string (or `undefined` if not set) and must return either:
|
|
498
|
+
- `{ success: true, value: T }` on success
|
|
499
|
+
- `{ success: false, error: string }` on failure
|
|
500
|
+
|
|
501
|
+
The return type `T` is inferred automatically — no manual type annotations needed.
|
|
502
|
+
|
|
503
|
+
## Framework Guides
|
|
504
|
+
|
|
505
|
+
### Node.js / Express / Fastify
|
|
506
|
+
|
|
507
|
+
No configuration needed. env-validated reads `process.env` by default.
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
// src/env.ts
|
|
511
|
+
import { createEnv } from 'env-validated'
|
|
512
|
+
|
|
513
|
+
export const env = createEnv({
|
|
514
|
+
schema: {
|
|
515
|
+
PORT: { type: 'port', default: 3000 },
|
|
516
|
+
DATABASE_URL: { type: 'url' },
|
|
517
|
+
NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'] as const },
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
// src/app.ts
|
|
522
|
+
import { env } from './env.js'
|
|
523
|
+
app.listen(env.PORT)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Next.js
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
// src/env.ts
|
|
530
|
+
import { createEnv } from 'env-validated'
|
|
531
|
+
|
|
532
|
+
// Server-side env vars
|
|
533
|
+
export const env = createEnv({
|
|
534
|
+
schema: {
|
|
535
|
+
DATABASE_URL: { type: 'url' },
|
|
536
|
+
API_SECRET: { type: 'string', minLength: 32 },
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
// Client-side env vars (with prefix stripping)
|
|
541
|
+
export const clientEnv = createEnv(
|
|
542
|
+
{
|
|
543
|
+
schema: {
|
|
544
|
+
API_URL: { type: 'url' },
|
|
545
|
+
APP_NAME: { type: 'string' },
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
{ source: process.env, prefix: 'NEXT_PUBLIC_' }
|
|
549
|
+
)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Vite
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
import { createEnv } from 'env-validated'
|
|
556
|
+
|
|
557
|
+
export const env = createEnv(
|
|
558
|
+
{
|
|
559
|
+
schema: {
|
|
560
|
+
API_URL: { type: 'url' },
|
|
561
|
+
DEBUG: { type: 'boolean', default: false },
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
{ source: import.meta.env, prefix: 'VITE_' }
|
|
565
|
+
)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Remix
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
// app/env.server.ts
|
|
572
|
+
import { createEnv } from 'env-validated'
|
|
573
|
+
|
|
574
|
+
export const env = createEnv({
|
|
575
|
+
schema: {
|
|
576
|
+
DATABASE_URL: { type: 'url' },
|
|
577
|
+
SESSION_SECRET: { type: 'string', minLength: 32 },
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Cloudflare Workers
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
// In your fetch handler
|
|
586
|
+
export default {
|
|
587
|
+
async fetch(request: Request, cfEnv: Env) {
|
|
588
|
+
const env = createEnv(
|
|
589
|
+
{
|
|
590
|
+
schema: {
|
|
591
|
+
API_KEY: { type: 'string', minLength: 16 },
|
|
592
|
+
REGION: { type: 'enum', values: ['us', 'eu'] as const },
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
{ source: cfEnv as Record<string, string> }
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
// env.API_KEY is typed and validated
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Deno
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
import { createEnv } from 'env-validated'
|
|
607
|
+
|
|
608
|
+
const env = createEnv(
|
|
609
|
+
{
|
|
610
|
+
schema: {
|
|
611
|
+
PORT: { type: 'port', default: 8000 },
|
|
612
|
+
ENV: { type: 'enum', values: ['dev', 'prod'] as const },
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
{ source: Deno.env.toObject() }
|
|
616
|
+
)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### NestJS
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
// src/env.ts
|
|
623
|
+
import { createEnv } from 'env-validated'
|
|
624
|
+
|
|
625
|
+
export const env = createEnv({
|
|
626
|
+
schema: {
|
|
627
|
+
PORT: { type: 'port', default: 3000 },
|
|
628
|
+
DATABASE_URL: { type: 'url' },
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
// src/app.module.ts
|
|
633
|
+
import { env } from './env.js'
|
|
634
|
+
|
|
635
|
+
@Module({
|
|
636
|
+
imports: [
|
|
637
|
+
TypeOrmModule.forRoot({ url: env.DATABASE_URL }),
|
|
638
|
+
],
|
|
639
|
+
})
|
|
640
|
+
export class AppModule {}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Testing (Jest / Vitest)
|
|
644
|
+
|
|
645
|
+
Pass a plain object as `source` for full isolation:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import { createEnv } from 'env-validated'
|
|
649
|
+
|
|
650
|
+
const env = createEnv(
|
|
651
|
+
{
|
|
652
|
+
schema: {
|
|
653
|
+
API_URL: { type: 'url' },
|
|
654
|
+
PORT: { type: 'port', default: 3000 },
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
source: {
|
|
659
|
+
API_URL: 'https://test.example.com',
|
|
660
|
+
PORT: '4000',
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
## Options
|
|
667
|
+
|
|
668
|
+
| Option | Type | Default | Description |
|
|
669
|
+
|--------|------|---------|-------------|
|
|
670
|
+
| `source` | `Record<string, string \| undefined>` | `process.env` | Custom env source |
|
|
671
|
+
| `onError` | `'throw' \| 'warn' \| (err: EnvSafeError) => void` | `'throw'` | Error handling strategy |
|
|
672
|
+
| `dotenv` | `boolean \| string` | `undefined` | Auto-load `.env` file (requires `dotenv` installed) |
|
|
673
|
+
| `prefix` | `string` | `undefined` | Strip prefix from env keys (e.g. `'VITE_'`, `'NEXT_PUBLIC_'`) |
|
|
674
|
+
|
|
675
|
+
### Error Handling
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
// Default: throws EnvSafeError with all failures
|
|
679
|
+
createEnv({ schema: { ... } })
|
|
680
|
+
|
|
681
|
+
// Warn to console instead of throwing
|
|
682
|
+
createEnv({ schema: { ... } }, { onError: 'warn' })
|
|
683
|
+
|
|
684
|
+
// Custom handler
|
|
685
|
+
createEnv({ schema: { ... } }, {
|
|
686
|
+
onError: (err) => {
|
|
687
|
+
logger.error(err.message)
|
|
688
|
+
process.exit(1)
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
// Catch programmatically
|
|
693
|
+
import { createEnv, EnvSafeError } from 'env-validated'
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const env = createEnv({ schema: { ... } })
|
|
697
|
+
} catch (e) {
|
|
698
|
+
if (e instanceof EnvSafeError) {
|
|
699
|
+
console.log(e.errors) // [{ key: 'PORT', message: '...' }, ...]
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## Secret Redaction
|
|
705
|
+
|
|
706
|
+
Variables whose names end with `_KEY`, `_SECRET`, `_TOKEN`, `_PASSWORD`, or `_PASS` are automatically masked in error output. Their values are never printed to the console.
|
|
707
|
+
|
|
708
|
+
```
|
|
709
|
+
[env-validated] Missing or invalid environment variables:
|
|
710
|
+
✖ API_KEY — must be at least 32 characters (got *****)
|
|
711
|
+
✖ DB_PASSWORD — required, was not set
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
The actual value is still validated normally.
|
|
715
|
+
|
|
716
|
+
## TypeScript Inference
|
|
717
|
+
|
|
718
|
+
Types are inferred automatically from the schema. No type casting or manual type declarations needed.
|
|
719
|
+
|
|
720
|
+
**Built-in types:**
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
const env = createEnv({
|
|
724
|
+
schema: {
|
|
725
|
+
PORT: { type: 'number', default: 3000 },
|
|
726
|
+
DRY_RUN: { type: 'boolean', default: false },
|
|
727
|
+
ENV: { type: 'enum', values: ['dev', 'prod'] as const },
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
env.PORT // number
|
|
732
|
+
env.DRY_RUN // boolean
|
|
733
|
+
env.ENV // 'dev' | 'prod'
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**External validators** — types pass through natively:
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
import { z } from 'zod'
|
|
740
|
+
|
|
741
|
+
const env = createEnv({
|
|
742
|
+
schema: {
|
|
743
|
+
COUNT: z.coerce.number().int().positive(),
|
|
744
|
+
TAGS: z.string().transform(s => s.split(',')),
|
|
745
|
+
}
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
env.COUNT // number
|
|
749
|
+
env.TAGS // string[]
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Custom validate functions** — return type is inferred:
|
|
753
|
+
|
|
754
|
+
```ts
|
|
755
|
+
const env = createEnv({
|
|
756
|
+
schema: {
|
|
757
|
+
IPS: {
|
|
758
|
+
validate: (val) =>
|
|
759
|
+
val
|
|
760
|
+
? { success: true as const, value: val.split(',') }
|
|
761
|
+
: { success: false as const, error: 'required' },
|
|
762
|
+
},
|
|
763
|
+
}
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
env.IPS // string[]
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Object-level schemas** — full output type is extracted:
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
import { z } from 'zod'
|
|
773
|
+
|
|
774
|
+
const env = createEnv({
|
|
775
|
+
schema: z.object({
|
|
776
|
+
HOST: z.string(),
|
|
777
|
+
PORT: z.coerce.number(),
|
|
778
|
+
})
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
env.HOST // string
|
|
782
|
+
env.PORT // number
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
> **Tip:** Use `as const` on enum `values` arrays to get narrow union types instead of `string`.
|
|
786
|
+
|
|
787
|
+
## Comparison
|
|
788
|
+
|
|
789
|
+
| Feature | t3-env | envalid | env-validated |
|
|
790
|
+
|---------|--------|---------|----------|
|
|
791
|
+
| Zod required | Yes | No | No (optional) |
|
|
792
|
+
| TS inference | Yes | No | Yes |
|
|
793
|
+
| Framework agnostic | Partial | Yes | Yes |
|
|
794
|
+
| Custom env source | No | No | Yes |
|
|
795
|
+
| Zero dependencies | No | No | Yes |
|
|
796
|
+
| Pluggable validators | No | No | Yes (9 adapters) |
|
|
797
|
+
| Object-level schemas | Partial | No | Yes |
|
|
798
|
+
| Mix validators per field | No | No | Yes |
|
|
799
|
+
| Community adapters | No | No | Yes |
|
|
800
|
+
|
|
801
|
+
## Supported Adapters
|
|
802
|
+
|
|
803
|
+
| Library | Auto-detected | Type Inference | Object Schema |
|
|
804
|
+
|---------|---------------|----------------|---------------|
|
|
805
|
+
| [Zod](https://github.com/colinhacks/zod) | Yes | Full | `z.object()` |
|
|
806
|
+
| [Joi](https://github.com/hapijs/joi) | Yes | `string` / `number` / `boolean` / `Date` | Via `joiObject()` |
|
|
807
|
+
| [Yup](https://github.com/jquense/yup) | Yes | Full | `yup.object()` |
|
|
808
|
+
| [Valibot](https://github.com/fabian-hiller/valibot) | Yes | Full | `v.object()` |
|
|
809
|
+
| [TypeBox](https://github.com/sinclairzx81/typebox) | Yes | Full | `Type.Object()` |
|
|
810
|
+
| [ArkType](https://github.com/arktypeio/arktype) | Yes | Full | `type({...})` |
|
|
811
|
+
| [Superstruct](https://github.com/ianstormtaylor/superstruct) | Yes | Full | `object()` |
|
|
812
|
+
| [Runtypes](https://github.com/runtypes/runtypes) | Yes | Full | `Object()` |
|
|
813
|
+
| [Effect Schema](https://github.com/Effect-TS/effect) | Yes | Full | `Schema.Struct()` |
|
|
814
|
+
|
|
815
|
+
## Community Adapters
|
|
816
|
+
|
|
817
|
+
The validator contract is a simple public interface. Anyone can publish their own adapter as a separate npm package:
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
import { registerAdapter } from 'env-validated'
|
|
821
|
+
|
|
822
|
+
registerAdapter({
|
|
823
|
+
name: 'my-validator',
|
|
824
|
+
detect: (field) => /* return true if this adapter handles the field */,
|
|
825
|
+
validate: (field, raw, key) => {
|
|
826
|
+
// return { success: true, value: parsed } or { success: false, error: 'message' }
|
|
827
|
+
},
|
|
828
|
+
})
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
## License
|
|
832
|
+
|
|
833
|
+
[MIT](./LICENSE)
|