npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@toolcode/envx

v0.1.1

Published

Type-safe config loader with Zod, hot-reload and deep-merge

Readme

@toolcode/envx

npm version License: MIT Node.js Version TypeScript

@toolcode/envx is a modern, type-safe configuration library for Node.js and TypeScript applications. It combines the power of Zod schema validation with the flexibility of object-based configuration files (JSON, YAML, TOML) and environment variable expansion.

Features

  • 🔒 Type-Safe: Returns strict TypeScript types inferred from your Zod schema.
  • 📄 Structured Config: Support for nested env.json, env.yaml, or env.toml. No more flat .env hell.
  • 🔥 Hot Reload: Automatically updates configuration when files change without restarting the app.
  • 🔗 Deep Merge: Merges multiple configuration files (e.g., default -> local).
  • 💲 Env Expansion: Supports bash-style variable substitution ${VAR} and defaults ${VAR:-default}.
  • 🚀 Process Env Sync: Optionally flattens and writes config back to process.env.

Installation

npm install @toolcode/envx zod

For YAML support:

npm install js-yaml

For TOML support:

npm install @iarna/toml

Requirements:

  • Node.js >= 18.0.0
  • TypeScript >= 5.3 (optional, but recommended)

Basic Usage

1. Define your schema

// config.ts
import { z } from 'zod';
import { loadConfig } from '@toolcode/envx';

const ConfigSchema = z.object({
  server: z.object({
    port: z.number().default(3000),
    host: z.string(),
  }),
  database: z.object({
    url: z.string(),
    password: z.string(),
  }),
});

// Load configuration asynchronously
const config = await loadConfig({
  schema: ConfigSchema,
  files: ['env.json', 'env.local.json'],
});

// config is fully typed!
console.log(config.server.port); // TypeScript knows this is a number

2. Synchronous loading

For cases where you need synchronous loading:

import { loadConfigSync } from '@toolcode/envx';

const config = loadConfigSync({
  schema: ConfigSchema,
  files: ['env.json'],
});

3. Create config file (env.json)

{
  "server": {
    "host": "0.0.0.0"
  },
  "database": {
    "url": "postgres://localhost:5432/mydb",
    "password": "${DB_PASSWORD:-secret}"
  }
}

4. Using different file formats

YAML (env.yaml):

server:
  host: "0.0.0.0"
  port: 3000
database:
  url: "postgres://localhost:5432/mydb"
  password: "${DB_PASSWORD:-secret}"

TOML (env.toml):

[server]
host = "0.0.0.0"
port = 3000

[database]
url = "postgres://localhost:5432/mydb"
password = "${DB_PASSWORD:-secret}"

Note: For TOML support, install @iarna/toml:

npm install @iarna/toml

Hot Reloading

Use watchConfig to react to file changes.

Note: envx updates the configuration object, but it is up to your application logic to apply those changes (e.g., reconnecting a database or restarting a server listener).

import { watchConfig } from '@toolcode/envx';

const handle = watchConfig({
  schema: ConfigSchema,
  files: ['env.json'],
  onUpdate: (newConfig) => {
    console.log('Config updated!', newConfig.server.port);
    // Example: restartServer(newConfig.server.port);
  },
  onError: (err) => {
    console.error('Invalid config update, keeping old config.', err.message);
  }
});

// To stop watching:
// handle.dispose();

Environment Expansion

envx supports syntax similar to bash:

  • ${VAR}: Replaced with value of process.env.VAR.
  • ${VAR:-default}: Used if VAR is missing or empty.

Sync with process.env

If you need to support legacy libraries that read from process.env:

await loadConfig({
  schema: ConfigSchema,
  processEnv: {
    enabled: true,
    flattenStrategy: 'UPPER_SNAKE_CASE',
    stringifyObjects: true // Convert objects/arrays to JSON strings
  }
});

// Nested config becomes flat env vars:
// { server: { port: 3000 } } => process.env.SERVER_PORT = "3000"
// { tags: ['api', 'v1'] } => process.env.TAGS = '["api","v1"]'

Note: envx only sets process.env variables that don't already exist, preserving existing environment variables.

CLI

The CLI is useful for validating configuration files in CI/CD pipelines without running the application.

# Check if files are valid (syntax check)
npx envx validate

# Check specific files
npx envx validate --files env.json env.local.json

# Print merged and expanded config (secrets redacted by default)
npx envx print

# Print with secrets visible (use with caution!)
npx envx print --show-secrets

# Print as flat .env format (useful for admin panels or docker-compose)
npx envx print --format env

# Print specific files
npx envx print --files env.json env.local.json

API Reference

loadConfig<T>(options: EnvXOptions<T>): Promise<z.infer<T>>

Loads configuration asynchronously from files and validates against a Zod schema.

loadConfigSync<T>(options: EnvXOptions<T>): z.infer<T>

Loads configuration synchronously. Use when you can't use async/await.

watchConfig<T>(options: WatchOptions<T>): WatchHandle

Watches configuration files for changes and triggers callbacks on updates.

Options:

  • schema: Zod schema for validation
  • files: Array of file paths to load (order matters for merging)
  • required: Throw error if no files found (default: false)
  • expandEnv: Enable environment variable expansion (default: true)
  • processEnv: Configuration for syncing to process.env
    • enabled: Enable syncing to process.env (default: false)
    • flattenStrategy: How to flatten nested objects (default: 'UPPER_SNAKE_CASE')
    • stringifyObjects: Convert non-primitive values to JSON strings (default: true)
  • onUpdate: Callback for successful config updates (watch mode only)
  • onError: Callback for validation/parse errors (watch mode only)

Advanced Examples

Multiple configuration files with merging

const config = await loadConfig({
  schema: ConfigSchema,
  files: [
    'config/default.json',  // Base config
    'config/production.json', // Environment-specific
    'config/local.json'      // Local overrides (gitignored)
  ]
});

// Later files override earlier ones, with deep merging

Handling missing files

const config = await loadConfig({
  schema: ConfigSchema,
  files: ['env.json', 'env.local.json'],
  required: false // Don't throw if files don't exist
});

// Or require at least one file:
const config = await loadConfig({
  schema: ConfigSchema,
  files: ['env.json', 'env.local.json'],
  required: true // Throw if no files found
});

Complex schema with nested objects

const ConfigSchema = z.object({
  server: z.object({
    port: z.number().min(1).max(65535),
    host: z.string().default('localhost'),
    cors: z.object({
      enabled: z.boolean().default(false),
      origins: z.array(z.string()).default([])
    })
  }),
  database: z.object({
    url: z.string().url(),
    pool: z.object({
      min: z.number().default(2),
      max: z.number().default(10)
    }).optional()
  }),
  features: z.record(z.string(), z.boolean()).optional()
});

Error Handling

envx throws ConfigError for validation failures and file issues. Always wrap config loading in try-catch:

import { loadConfig, ConfigError } from '@toolcode/envx';

try {
  const config = await loadConfig({
    schema: ConfigSchema,
    files: ['env.json']
  });
} catch (error) {
  if (error instanceof ConfigError) {
    console.error('Configuration error:', error.message);
    // Handle config-specific errors
  } else {
    console.error('Unexpected error:', error);
  }
  process.exit(1);
}

Common Errors

  • Configuration Validation Failed: Your config doesn't match the Zod schema. Check the error message for specific field issues.
  • No configuration files found: No files were found and required: true was set.
  • Failed to parse <file>: Syntax error in JSON/YAML/TOML file.
  • Configuration file <file> is too large: File exceeds 10MB limit.
  • Too many variable expansions: Possible ReDoS attack or circular reference in variable expansion.

Troubleshooting

Environment variables not expanding

If ${VAR} patterns aren't being replaced:

  1. Ensure expandEnv: true (default) is set
  2. Check that the variable exists in process.env
  3. Verify variable name uses only alphanumeric characters and underscores
  4. For .env files, call dotenv.config() before loading config:
import dotenv from 'dotenv';
dotenv.config(); // Load .env files first

import { loadConfig } from '@toolcode/envx';
const config = await loadConfig({ schema, files: ['env.json'] });

TypeScript types not inferred correctly

Make sure your Zod schema is properly typed:

// ✅ Good - types are inferred
const schema = z.object({
  port: z.number()
});
type Config = z.infer<typeof schema>; // { port: number }

// ❌ Bad - types are too loose
const schema: z.ZodTypeAny = z.object({
  port: z.number()
});

Hot reload not working

  • Ensure files are being watched (check file paths)
  • Verify file changes are saved completely (watcher waits 300ms for stability)
  • Check that validation passes after changes (invalid configs won't trigger onUpdate)

Files not merging correctly

Remember: later files override earlier ones with deep merging:

// base.json: { server: { port: 3000, host: 'localhost' } }
// local.json: { server: { port: 8080 } }
// Result: { server: { port: 8080, host: 'localhost' } }

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development

# Clone the repository
git clone https://github.com/Enot-Racoon/toolcode-envx.git
cd envx

# Install dependencies
npm install

# Run tests
npm test

# Build the project
npm run build

# Run demo
npm run demo

License

MIT © 2025 envx contributors