dotenv-but-good
v1.0.2
Published
dotenv with schema validation, multiple file loading, JSON env files, writing, variable expansion, and perfect types. no ads.
Maintainers
Readme
dotenv-but-good
dotenv, but with schema validation, multiple file loading, JSON env support, writing, variable expansion, and perfect TypeScript types.
also no ads in your shell.
npm i dotenv-but-goodQuick start
import { config } from 'dotenv-but-good';
config(); // loads .env into process.env, just like dotenvSchema validation
The killer feature. Define a schema and get back fully-typed env vars — no more process.env.FOO as string.
import { config } from 'dotenv-but-good';
const { typed } = config({
schema: {
PORT: { type: 'number', required: true, min: 1, max: 65535 },
DATABASE_URL: { type: 'url', required: true },
NODE_ENV: { type: 'string', enum: ['development', 'production', 'test'], default: 'development' },
DEBUG: { type: 'boolean', default: false },
API_KEY: { type: 'string', required: true, minLength: 32 },
}
});
// typed is fully inferred — no casting needed
typed.PORT; // number
typed.DATABASE_URL; // string
typed.DEBUG; // booleanThrows a clear error if anything is missing or wrong:
Env validation failed:
PORT: must be a number
API_KEY: is required but missingSchema field options
| Option | Types | Description |
|--------|-------|-------------|
| type | all | 'string' 'number' 'boolean' 'url' 'email' 'json' |
| required | all | throw if missing and no default |
| default | all | fallback value if not set |
| description | all | used by generateExample() |
| min / max | number | numeric range |
| integer | number | must be a whole number |
| minLength / maxLength | string | length constraints |
| pattern | string | must match regex |
| enum | string | must be one of these values |
| truthyValues / falsyValues | boolean | custom truthy/falsy strings (defaults: true/1/yes/on, false/0/no/off) |
| transform | all | (raw: string) => T custom coercion |
| validate | all | (value: T) => boolean \| string custom validation |
Standalone env() helper
If you've already called config() elsewhere and just want a typed snapshot of process.env:
import { env } from 'dotenv-but-good';
const e = env({
PORT: { type: 'number', required: true },
NODE_ENV: { type: 'string', default: 'development' },
});
e.PORT; // numberMultiple files
Pass an array of paths — later files override earlier ones.
config({
path: ['.env', '.env.local', `.env.${process.env.NODE_ENV}`],
ignoreMissing: true, // silently skip files that don't exist
});JSON env files
Any .json file is automatically parsed as an env file. Useful for secrets managers that export JSON.
// secrets.json
{
"API_KEY": "abc123",
"PORT": 3000,
"DEBUG": false
}config({ path: ['secrets.json', '.env'] });Values must be a flat object of primitives. Numbers and booleans are coerced to strings before being written to process.env.
Writing .env files
import { writeEnv } from 'dotenv-but-good';
writeEnv({
PORT: 3000,
NODE_ENV: 'production',
API_KEY: 'abc123',
});By default this merges with the existing .env file — existing keys not in your object are preserved. To replace everything:
writeEnv({ PORT: 3000 }, { mode: 'overwrite' });Options
writeEnv(vars, {
path: '.env.production', // default: .env in cwd
mode: 'merge', // 'merge' | 'overwrite'
sort: true, // sort keys alphabetically
group: true, // group keys by prefix with blank lines
header: 'Generated by deploy script. Do not edit manually.',
});Async version also available: writeEnvAsync().
Variable expansion
Resolve ${VAR} references within values:
import { parseEnv, expand } from 'dotenv-but-good';
const parsed = parseEnv(`
BASE_URL=https://example.com
API_URL=\${BASE_URL}/api/v1
`);
const expanded = expand(parsed);
// { BASE_URL: 'https://example.com', API_URL: 'https://example.com/api/v1' }Strict mode
Throw instead of returning errors:
config({ strict: true, schema: { ... } });
// throws if validation fails or file is missingGenerate a .env.example
Auto-generate a .env.example from your schema so teammates know what to fill in:
import { generateExample } from 'dotenv-but-good';
generateExample({
PORT: { type: 'number', required: true, description: 'Port the server listens on' },
DATABASE_URL: { type: 'url', required: true, description: 'Postgres connection string' },
DEBUG: { type: 'boolean', default: false },
});Outputs:
# Auto-generated .env.example
# Port the server listens on
# type: number, required
PORT=
# Postgres connection string
# type: url, required
DATABASE_URL=
# type: boolean, default: false
DEBUG=Parser details
The parser handles everything you'd expect and a few things you wouldn't:
# comments work, inline too
PORT=3000 # this is fine
# all quote styles supported
SINGLE='preserve whitespace exactly'
DOUBLE="process \n escape \t sequences"
BACKTICK=`also works`
# multiline
CERT="""
-----BEGIN CERTIFICATE-----
abc123
-----END CERTIFICATE-----
"""
# variable expansion
BASE=https://example.com
API=${BASE}/api
# export prefix
export SECRET=hunter2
# backslash continuation
LONG_VALUE=first part \
second partAsync API
Every file operation has an async counterpart:
import { configAsync, writeEnvAsync } from 'dotenv-but-good';
const { typed } = await configAsync({ schema: { ... } });
await writeEnvAsync({ PORT: 3000 });stringify()
Serialise a record to a .env string without writing to disk:
import { stringify } from 'dotenv-but-good';
const content = stringify({ PORT: 3000, DEBUG: true }, /* sort: */ true);
// PORT=3000
// DEBUG=trueAPI reference
| Export | Description |
|--------|-------------|
| config(options?) | Load .env file(s), inject into process.env, optionally validate |
| configAsync(options?) | Async version of config() |
| env(schema) | Validate process.env against a schema, return typed result |
| parseEnv(content) | Parse a .env string into a record |
| parseJsonEnv(content) | Parse a JSON string into a record |
| expand(parsed) | Expand ${VAR} references in a parsed record |
| validateSchema(parsed, schema) | Validate/coerce a record against a schema |
| writeEnv(vars, options?) | Write or merge vars into a .env file |
| writeEnvAsync(vars, options?) | Async version of writeEnv() |
| stringify(vars, sort?) | Serialise vars to a .env string |
| generateExample(schema, path?) | Write a .env.example from a schema |
License
MIT
