Skip to content
/ envox Public

Fast and flexible environment variable parser with detailed error reporting

License

Notifications You must be signed in to change notification settings

johnie/envox

Repository files navigation

🌿
envox

Fast and flexible environment variable parser with detailed error reporting.


License npm Build Status stars



Installation

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

Quick Start

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
}

Features

  • 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)

API Reference

parseEnv Function

The main function for parsing environment variables.

import { parseEnv } from "envox";

const result = parseEnv(source, options);

Function 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
}

parseEnv(source, options) Parameters

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;
}

Helper Functions

isEnvFile(content)

Check if content looks like an environment file.

import { isEnvFile } from "envox";

const content = `
  NODE_ENV=development
  PORT=3000
`;

console.log(isEnvFile(content)); // true

fromObject(obj, { includeExport })

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

Schema Validation

Envox supports any validation library that implements the Standard Schema specification. This includes popular libraries like Zod, Valibot, and Effect Schema.

With Zod

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);
}

With Valibot

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 });

Custom Schema

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 });

Options

Option Details

  • allowEmpty - When true, lines without = are ignored. When false, they cause parse errors.
  • allowComments - When true, lines starting with # are treated as comments.
  • allowExport - When true, supports export VAR=value syntax.
  • trimValues - When true, removes leading/trailing whitespace from values.
  • expandVariables - When true, expands ${VAR} and $VAR references.
  • schema - Optional Standard Schema for validation and transformation.

Working with process.env

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"
}

Using with dotenv

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);
}

Advanced dotenv integration

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;

Variable Expansion

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"
}

Quote Handling

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""
}

Error Handling

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}`);
  });
}

Validation Error Handling

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}`);
    }
  });
}

Examples

Basic Usage

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));
}

With Variable Expansion

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"
}

Comprehensive Example with Zod

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
}

Advanced Schema with Transformations

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
}

Error Handling with Detailed Reporting

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
}

TypeScript Integration

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
}

Performance

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 });

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

MIT License. See the LICENSE file for details.

About

Fast and flexible environment variable parser with detailed error reporting

Resources

License

Contributing

Stars

Watchers

Forks

Contributors 3

  •  
  •  
  •