Fast and flexible environment variable parser with detailed error reporting.
Install envox with your preferred package manager:
npm install envox
# or
pnpm add envox
# or
bun add envox
For schema validation, install a compatible validation library:
npm install zod valibot effect
# or your preferred validation library that supports Standard Schema
import { parseEnv } from "envox";
const envContent = `
# Database configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
# API settings
API_KEY="secret-key-123"
DEBUG=true
`;
const result = parseEnv(envContent);
if (result.ok) {
console.log(result.data); // { DB_HOST: 'localhost', DB_PORT: '5432', ... }
console.log(result.vars); // Array of variables with line numbers
} else {
console.error(result.errors); // Parse errors with line numbers
}
- Flexible parsing - Handle various .env file formats and process.env objects
- Detailed error reporting - Get line numbers and context for errors
- Variable expansion - Support for
${VAR}
and$VAR
syntax - Quote handling - Properly parse single and double quoted values
- Export support - Handle
export VAR=value
syntax - Schema validation - Validate and transform parsed values with any Standard Schema compatible library
- TypeScript support - Full type definitions included
- Zero dependencies - Lightweight and fast (validation libraries are optional)
The main function for parsing environment variables.
import { parseEnv } from "envox";
const result = parseEnv(source, options);
interface EnvoxOptions<T = Record<string, string>> {
allowEmpty?: boolean; // Default: true
allowComments?: boolean; // Default: true
allowExport?: boolean; // Default: true
trimValues?: boolean; // Default: true
expandVariables?: boolean; // Default: false
schema?: StandardSchemaV1<Record<string, string>, T>; // Optional schema
}
Parse environment variables from a string or object.
// Parse from string
const result = parseEnv(`
DATABASE_URL=postgres://localhost/mydb
PORT=3000
`);
// Parse from process.env or any object
const result = parseEnv(process.env);
const result = parseEnv({ API_KEY: "secret", DEBUG: "true" });
Returns:
type ParseResult<T> =
| { ok: true; data: T; vars: EnvVariable[] }
| { ok: false; errors: EnvoxParseError[] };
interface EnvVariable {
key: string;
value: string;
line: number;
}
interface EnvoxParseError {
line: number;
message: string;
content: string;
}
Check if content looks like an environment file.
import { isEnvFile } from "envox";
const content = `
NODE_ENV=development
PORT=3000
`;
console.log(isEnvFile(content)); // true
Convert a plain object to environment variable format.
import { fromObject } from "envox";
const obj = { API_KEY: "secret", DEBUG: "true" };
const envString = fromObject(obj, { includeExport: true }); // Include 'export' prefix
console.log(envString);
// export API_KEY=secret
// export DEBUG=true
Envox supports any validation library that implements the Standard Schema specification. This includes popular libraries like Zod, Valibot, and Effect Schema.
import { z } from "zod";
import { parseEnv } from "envox";
// Define your schema
const ConfigSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().int().positive(),
DATABASE_URL: z.string().url(),
DEBUG: z.coerce.boolean().default(false),
});
const envContent = `
NODE_ENV=development
PORT=3000
DATABASE_URL=https://localhost/db
DEBUG=true
`;
// Parse with validation
const result = parseEnv(envContent, { schema: ConfigSchema });
if (result.ok) {
console.log(result.data); // Fully typed and validated config
console.log(result.data.PORT); // number (3000)
console.log(result.data.DEBUG); // boolean (true)
} else {
console.error("Validation errors:", result.errors);
}
import * as v from "valibot";
import { parseEnv } from "envox";
const ConfigSchema = v.object({
API_KEY: v.string(),
MAX_CONNECTIONS: v.pipe(v.string(), v.transform(Number), v.number()),
ENABLE_SSL: v.pipe(
v.string(),
v.transform((val) => val === "true"),
v.boolean()
),
});
const result = parseEnv(envContent, { schema: ConfigSchema });
You can also create custom schemas that implement the Standard Schema interface:
import type { StandardSchemaV1 } from "@standard-schema/spec";
interface MyConfig {
name: string;
version: number;
}
const customSchema: StandardSchemaV1<Record<string, string>, MyConfig> = {
"~standard": {
version: 1,
vendor: "my-validator",
validate: (value) => {
const obj = value as Record<string, string>;
if (!obj.name || !obj.version) {
return {
issues: [
{ message: "name is required", path: ["name"] },
{ message: "version is required", path: ["version"] },
],
};
}
const version = parseInt(obj.version, 10);
if (isNaN(version)) {
return {
issues: [{ message: "version must be a number", path: ["version"] }],
};
}
return {
value: {
name: obj.name,
version: version,
},
};
},
},
};
const result = parseEnv(envContent, { schema: customSchema });
allowEmpty
- Whentrue
, lines without=
are ignored. Whenfalse
, they cause parse errors.allowComments
- Whentrue
, lines starting with#
are treated as comments.allowExport
- Whentrue
, supportsexport VAR=value
syntax.trimValues
- Whentrue
, removes leading/trailing whitespace from values.expandVariables
- Whentrue
, expands${VAR}
and$VAR
references.schema
- Optional Standard Schema for validation and transformation.
Envox can parse and validate variables directly from process.env
:
import { parseEnv } from "envox";
import { z } from "zod";
// Parse process.env with schema validation
const schema = z.object({
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production"]).default("development"),
API_KEY: z.string().min(1),
});
const result = parseEnv(process.env, { schema });
if (result.ok) {
console.log(result.data); // Typed and validated config
}
// Variable expansion also works with process.env
process.env.API_HOST = "api.example.com";
process.env.API_URL = "https://${API_HOST}/v1";
const expanded = parseEnv(process.env, { expandVariables: true });
if (expanded.ok) {
console.log(expanded.data.API_URL); // "https://api.example.com/v1"
}
Envox works great alongside dotenv for enhanced validation and type safety:
import "dotenv/config"; // Load .env file into process.env
import { parseEnv } from "envox";
import { z } from "zod";
// Define your configuration schema
const ConfigSchema = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
DEBUG: z.coerce.boolean().default(false),
});
// Validate and transform process.env after dotenv loads it
const result = parseEnv(process.env, { schema: ConfigSchema });
if (result.ok) {
const config = result.data;
// Now you have fully typed and validated configuration
console.log(config.PORT); // TypeScript knows this is a number
console.log(config.DEBUG); // TypeScript knows this is a boolean
export default config;
} else {
console.error("Configuration errors:", result.errors);
process.exit(1);
}
import dotenv from "dotenv";
import { parseEnv } from "envox";
import { z } from "zod";
// Load different .env files based on environment
const envFile = process.env.NODE_ENV === "test" ? ".env.test" : ".env";
dotenv.config({ path: envFile });
const ConfigSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
// Enable variable expansion for complex configurations
API_ENDPOINT: z.string().url(),
});
// Parse with detailed error reporting
const result = parseEnv(process.env, {
schema: ConfigSchema,
expandVariables: true,
});
if (!result.ok) {
console.error("Configuration errors:");
result.errors.forEach((error) => {
console.error(`- ${error.message} (line ${error.line})`);
});
process.exit(1);
}
const config = result.data;
When expandVariables
is enabled, you can reference previously defined variables:
const content = `
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1
FULL_URL=$API_ENDPOINT/users
`;
const result = parseEnv(content, { expandVariables: true });
if (result.ok) {
console.log(result.data.FULL_URL); // "https://api.example.com/v1/users"
}
Envox properly handles both single and double quotes:
const content = `
SINGLE='value with spaces'
DOUBLE="value with $pecial chars"
ESCAPED="value with \\"quotes\\""
`;
const result = parseEnv(content);
if (result.ok) {
console.log(result.data.SINGLE); // "value with spaces"
console.log(result.data.DOUBLE); // "value with $pecial chars"
console.log(result.data.ESCAPED); // "value with "quotes""
}
Envox provides detailed error information for both parsing and validation errors:
const result = parseEnv(`
VALID_VAR=okay
123_INVALID=bad
ANOTHER=fine
`);
if (!result.ok) {
result.errors.forEach((error) => {
console.log(`Line ${error.line}: ${error.message}`);
console.log(`Content: ${error.content}`);
});
}
When using schemas, validation errors are included in the errors array:
import { z } from "zod";
import { parseEnv } from "envox";
const schema = z.object({
PORT: z.coerce.number().min(1000),
API_KEY: z.string().min(10),
});
const result = parseEnv(
`
PORT=80
API_KEY=short
`,
{ schema }
);
if (!result.ok) {
result.errors.forEach((error) => {
if (error.line === 0) {
console.log(`Validation error: ${error.message}`);
} else {
console.log(`Parse error at line ${error.line}: ${error.message}`);
}
});
}
import { parseEnv } from "envox";
const result = parseEnv(`
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://localhost/mydb
`);
if (result.ok) {
// Use in your application
const server = createServer();
server.listen(parseInt(result.data.PORT));
}
import { parseEnv } from "envox";
const result = parseEnv(
`
APP_NAME=myapp
VERSION=1.0.0
IMAGE_TAG=${APP_NAME}:${VERSION}
CONTAINER_NAME=${APP_NAME}-container
`,
{ expandVariables: true }
);
if (result.ok) {
console.log(result.data.IMAGE_TAG); // "myapp:1.0.0"
console.log(result.vars.find((v) => v.key === "IMAGE_TAG")?.value); // "myapp:1.0.0"
}
import { z } from "zod";
import { parseEnv } from "envox";
// Define a comprehensive schema
const AppConfigSchema = z.object({
// Server settings
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
HOST: z.string().default("localhost"),
// Database
DATABASE_URL: z.string().url(),
DB_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),
// API settings
API_KEY: z.string().min(32),
API_TIMEOUT: z.coerce.number().int().min(1000).default(5000),
// Feature flags
ENABLE_LOGGING: z.coerce.boolean().default(true),
ENABLE_METRICS: z.coerce.boolean().default(false),
// Optional settings
REDIS_URL: z.string().url().optional(),
SENTRY_DSN: z.string().url().optional(),
});
type AppConfig = z.infer<typeof AppConfigSchema>;
// Load and validate environment
const envContent = `
NODE_ENV=production
PORT=8080
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
API_KEY=super-secret-api-key-that-is-long-enough
ENABLE_LOGGING=true
ENABLE_METRICS=true
`;
const result = parseEnv(envContent, { schema: AppConfigSchema });
if (result.ok) {
const config: AppConfig = result.data;
console.log("Configuration loaded successfully:");
console.log(`Server will run on ${config.HOST}:${config.PORT}`);
console.log(`Environment: ${config.NODE_ENV}`);
console.log(`Logging enabled: ${config.ENABLE_LOGGING}`);
// All values are properly typed and validated
startServer(config);
} else {
console.error("Failed to load configuration:");
result.errors.forEach((error) => console.error(error.message));
process.exit(1);
}
function startServer(config: AppConfig) {
// Your application logic here
// All config values are guaranteed to be valid
}
import { z } from "zod";
import { parseEnv } from "envox";
const AdvancedSchema = z.object({
// Transform comma-separated values to array
ALLOWED_ORIGINS: z
.string()
.transform((val) => val.split(",").map((s) => s.trim())),
// Parse JSON configuration
FEATURE_FLAGS: z.string().transform((val) => {
try {
return JSON.parse(val);
} catch {
throw new Error("FEATURE_FLAGS must be valid JSON");
}
}),
// Custom validation for log level
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
// Transform string to Date
DEPLOYMENT_DATE: z
.string()
.datetime()
.transform((val) => new Date(val)),
// Conditional validation
CACHE_TTL: z.coerce.number().min(60).max(86400),
});
const result = parseEnv(
`
ALLOWED_ORIGINS=https://example.com, https://app.example.com
FEATURE_FLAGS={"newUI": true, "betaFeatures": false}
LOG_LEVEL=info
DEPLOYMENT_DATE=2024-01-15T10:30:00Z
CACHE_TTL=3600
`,
{ schema: AdvancedSchema }
);
if (result.ok) {
console.log(result.data.ALLOWED_ORIGINS); // ['https://example.com', 'https://app.example.com']
console.log(result.data.FEATURE_FLAGS); // { newUI: true, betaFeatures: false }
console.log(result.data.DEPLOYMENT_DATE); // Date object
}
import { z } from "zod";
import { parseEnv } from "envox";
const StrictSchema = z.object({
API_URL: z.string().url("Must be a valid URL"),
MAX_RETRIES: z.coerce.number().int().min(1).max(10),
TIMEOUT_MS: z.coerce.number().int().min(100),
});
const problematicEnv = `
# This will have multiple issues
API_URL=not-a-url
MAX_RETRIES=20
TIMEOUT_MS=50
123_INVALID=value
`;
const result = parseEnv(problematicEnv, { schema: StrictSchema });
if (!result.ok) {
console.log("Environment validation failed:");
result.errors.forEach((error, index) => {
if (error.line > 0) {
console.log(
`${index + 1}. Parse error (line ${error.line}): ${error.message}`
);
} else {
console.log(`${index + 1}. Validation error: ${error.message}`);
}
});
// Output might be:
// 1. Parse error (line 6): Invalid environment variable key: 123_INVALID
// 2. Validation error: Must be a valid URL
// 3. Validation error: Number must be less than or equal to 10
// 4. Validation error: Number must be greater than or equal to 100
}
Envox provides excellent TypeScript support with full type inference when using schemas:
import { z } from "zod";
import { parseEnv } from "envox";
const ConfigSchema = z.object({
API_KEY: z.string(),
PORT: z.coerce.number(),
DEBUG: z.coerce.boolean(),
});
const result = parseEnv(envContent, { schema: ConfigSchema });
if (result.ok) {
// Type is automatically inferred as:
// { API_KEY: string; PORT: number; DEBUG: boolean; }
const config = result.data;
// TypeScript knows the exact types
config.PORT.toFixed(2); // ✅ number method
config.DEBUG ? "yes" : "no"; // ✅ boolean
config.API_KEY.toLowerCase(); // ✅ string method
}
Envox is designed to be fast and lightweight:
- Zero dependencies for core functionality
- Efficient parsing with minimal allocations
- Optional schema validation only when needed
- Supports both sync workflows
// For performance-critical applications, you can skip validation
const result = parseEnv(content); // No schema = faster parsing
// Or use validation only in development
const schema = process.env.NODE_ENV === "development" ? MySchema : undefined;
const result = parseEnv(content, { schema });
We welcome contributions! Please see our Contributing Guide for details.
MIT License. See the LICENSE file for details.