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

zfig

v0.9.15

Published

Dev-friendly TypeScript configuration library with Zod

Readme

Install

npm install zfig zod

Schema & Field

schema(definition)

Creates a type-safe config schema from a definition object.

import { schema, field } from "zfig";
import { z } from "zod";

const config = schema({
  appName: "my-app",                    // literal value
  port: field({ type: z.number() }),    // field config
  db: {                                 // nested object
    host: field({ type: z.string() }),
  },
});

Definition values can be:

  • Literals - strings, numbers, booleans (become z.literal())
  • Field configs - created with field()
  • Nested objects - recursively processed
  • Raw Zod types - passed through directly

field(config)

Marks a config field with resolution metadata.

field({
  type: z.string(),           // required - Zod type validator
  env: "DB_HOST",             // env var name
  secretFile: "db-password",  // path to secret file
  sensitive: true,            // redact in logs/errors
  default: "localhost",       // fallback value
  doc: "Database hostname",   // description (becomes .describe())
})

Field Options

| Option | Type | Description | |--------|------|-------------| | type | ZodType | Required. Zod schema for validation | | env | string | Environment variable name | | secretFile | string | Path to file containing secret value | | sensitive | boolean | Redact value in toString/errors/debug | | default | unknown | Default value if no source provides one | | doc | string | Documentation (converted to Zod .describe()) |

Literals & Nesting

Literals become z.literal() types:

schema({
  version: "1.0",     // z.literal("1.0")
  port: 3000,         // z.literal(3000)
  debug: true,        // z.literal(true)
});

Nesting supports arbitrary depth:

schema({
  db: {
    primary: {
      host: field({ type: z.string() }),
      port: field({ type: z.number() }),
    },
    replica: {
      host: field({ type: z.string() }),
    },
  },
});

Composable Schemas

Schemas can be nested inside other schemas. Metadata is preserved.

const dbSchema = schema({
  host: field({ type: z.string(), env: "DB_HOST" }),
  port: field({ type: z.number(), default: 5432 }),
});

const appSchema = schema({
  db: dbSchema,
  name: field({ type: z.string() }),
});

// dbSchema metadata accessible via appSchema.shape.db.shape.host.meta()

Config Files

Using resolve()

import { resolve } from "zfig";

const config = resolve(configSchema, {
  configPath: "./config.json",
});

CONFIG_PATH Environment Variable

If configPath not provided, resolve() reads from CONFIG_PATH env var:

CONFIG_PATH=./config.json node app.js

JSON Support

JSON is supported by default:

{
  "db": {
    "host": "localhost",
    "port": 5432
  }
}

YAML Support

Install @zfig/yaml-loader for YAML support:

npm install @zfig/yaml-loader
import "@zfig/yaml-loader"; // side-effect import registers loader
import { resolve } from "zfig";

const config = resolve(configSchema, { configPath: "./config.yaml" });

Initial Values

Provide baseline values that can be overridden by config files, env vars, or override:

const config = resolve(configSchema, {
  initialValues: { db: { host: "dev-host", port: 5433 } },
  configPath: "./config.json",
});

Resolution priority: override > env > secretFile > configFile > initialValues > default.

Use cases:

  • Programmatic defaults that differ from schema defaults
  • Framework/library defaults that apps can override
  • Test fixtures with sensible baseline values

Type Coercion

Use z.coerce.* for automatic type conversion from env vars:

schema({
  port: field({ type: z.coerce.number(), env: "PORT" }),      // "8080" → 8080
  debug: field({ type: z.coerce.boolean(), env: "DEBUG" }),   // "true" → true
});

Debugging

With multiple config sources (env, files, secrets, defaults), it's easy to lose track of where a value came from. Source tracing helps you answer: "Why is the database connecting to the wrong host?"

Why You Need This

Scenario: Your app connects to the wrong database in staging.

const config = resolve(configSchema, { configPath: "./config.json" });
console.log(config.db.host); // "prod-db.example.com" — but why?

Without source tracing, you'd have to manually check: env vars? config file? secrets? defaults?

With zfig, just ask:

import { getSources } from "zfig";

console.log(getSources(config));
// {
//   "db.host": "env:DB_HOST",        ← env var is overriding your config file!
//   "db.port": "file:./config.json",
//   "db.password": "secretFile:db-password"
// }

Now you know: someone set DB_HOST in the environment, overriding your config file.

Source Tracking

import { resolve, getSources } from "zfig";

const config = resolve(configSchema, { configPath: "./config.json" });

// Map of field path → source identifier
getSources(config);
// { "db.host": "env:DB_HOST", "db.port": "file:./config.json", "name": "default" }

// As JSON string (useful for logging)
config.toSourceString();
// '{"db.host":"env:DB_HOST","db.port":"file:./config.json","name":"default"}'

Source identifiers: | Identifier | Meaning | |------------|---------| | env:VAR_NAME | Environment variable | | file:./path | Config file | | secretFile:name | Secret file | | default | Schema default value | | initial | initialValues option | | override | override option | | literal | Literal value in schema |

Debug Object

Get values and sources together — useful for startup logs or admin endpoints:

config.toDebugObject();
// {
//   config: {
//     "db": {
//        "host": { value: "prod-db.example.com", source: "env:DB_HOST" },
//        "port": { value: 5432, source: "file:./config.json" },
//        "password": { value: "[REDACTED]", source: "secretFile:db-password" }
//     }
//   }
// }

Sensitive values are automatically redacted.

Diagnostics

For deeper debugging, get the full resolution trace — what sources were checked for each value:

import { getDiagnostics } from "zfig";

getDiagnostics(config);
// [
//   { type: "configPath", picked: "./config.json", candidates: ["option:./config.json"], reason: "provided" },
//   { type: "loader", format: ".json", used: true },
//   { type: "sourceDecision", key: "db.host", picked: "env:DB_HOST", tried: ["env:DB_HOST", "file:./config.json", "default"] },
//   { type: "sourceDecision", key: "db.port", picked: "file:./config.json", tried: ["env:DB_PORT", "file:./config.json", "default"] }
// ]

The tried array shows all sources checked in priority order. Useful when you expected a value from one source but another took precedence.

Event types:

  • configPath — which config file was selected and why
  • loader — which file format loader was used
  • sourceDecision — which source provided each value, and what else was tried
  • note — additional info messages

Include diagnostics in debug object:

config.toDebugObject({ includeDiagnostics: true });
// { config: {...}, diagnostics: [...] }

Sensitive Values

Mark fields as sensitive to prevent accidental exposure:

schema({
  apiKey: field({
    type: z.string(),
    env: "API_KEY",
    sensitive: true,
  }),
});

const config = resolve(configSchema);

config.toString();
// '{"apiKey":"[REDACTED]"}'

config.toDebugObject();
// { config: { apiKey: { value: "[REDACTED]", source: "env:API_KEY" } } }

Sensitive values are redacted in:

  • toString() output
  • toDebugObject() output
  • Error messages

Loader Registry

Register custom loaders for different file formats:

import { registerLoader, getLoader, getSupportedExtensions, clearLoaders } from "zfig";

// Register a loader
registerLoader(".toml", (path) => {
  const content = fs.readFileSync(path, "utf-8");
  return toml.parse(content);
});

// Get loader for extension
const loader = getLoader(".toml");

// List supported extensions
getSupportedExtensions(); // [".json", ".toml"]

// Clear all loaders
clearLoaders();

Loader signature:

type FileLoader = (path: string) => Record<string, unknown> | undefined;

Return undefined if file doesn't exist. Throw on parse errors.

Error Handling

ConfigError is thrown when resolution fails:

import { ConfigError } from "zfig";

try {
  const config = resolve(configSchema);
} catch (e) {
  if (e instanceof ConfigError) {
    console.log(e.message);     // error description
    console.log(e.path);        // "db.host" (dot-notation path)
    console.log(e.sensitive);   // true if value should be redacted
    console.log(e.diagnostics); // diagnostic events collected before error
  }
}

Thrown when:

  • Required field has no value from any source
  • Zod validation fails
  • Config file has invalid JSON/YAML
  • File extension has no registered loader

Troubleshooting

Missing required config

ConfigError: Missing required config value at path "db.password"

Provide value via env var, secret file, config file, or default.

Type validation failed

ConfigError: Validation failed at path "port": Expected number, received string

Use z.coerce.number() for env vars that need type conversion.

Unsupported file extension

ConfigError: No loader registered for extension ".yaml"

Install and import @zfig/yaml-loader for YAML support.

Config file not found

Check configPath option or CONFIG_PATH env var. JSON loader returns undefined for missing files (no error).

Secrets not loading

  • Check secretFile path is correct
  • Default secrets base path is /secrets
  • Use secretsPath option in resolve to change base path

Advanced Usage

All Zod features work in field types - .coerce, .nonempty(), .min(), .transform(), etc. Validation is fully delegated to Zod, so you can use any schema features. The only exception is .meta() which zfig uses internally for field metadata and will be overridden.

schema({
  port: field({ type: z.coerce.number().min(1).max(65535), env: "PORT" }),
  tags: field({ type: z.array(z.string()).nonempty(), default: ["default"] }),
  email: field({ type: z.string().email(), env: "ADMIN_EMAIL" }),
});

API Reference

Core Functions

| Function | Description | |----------|-------------| | schema(definition) | Create config schema | | field(config) | Create field with metadata | | resolve(schema, options?) | Resolve values with file loading | | resolveValues(schema, options?) | Resolve values without file loading | | getSources(config) | Get source map from resolved config | | getDiagnostics(config) | Get diagnostic events from resolved config |

Loader Registry

| Function | Description | |----------|-------------| | registerLoader(ext, loader) | Register file loader for extension | | getLoader(ext) | Get loader for extension | | getSupportedExtensions() | List registered extensions | | clearLoaders() | Remove all loaders |

Error Class

| Class | Description | |-------|-------------| | ConfigError | Error with path and sensitive properties |

Resolve Options

resolve(schema, {
  configPath?: string,           // path to config file
  env?: Record<string, string>,  // env vars (default: process.env)
  secretsPath?: string,          // base path for secrets (default: "/secrets")
  initialValues?: object,        // base values
  override?: object,             // override all sources
});

Performance

zfig is designed for startup-time config loading where correctness and debuggability matter more than raw speed. That said, it performs well:

| Scenario | zfig | vs zod-config | vs convict | vs @t3-oss/env-core | |----------|--------|---------------|------------|---------------------| | Env only | 704K ops/sec | - | - | 20x faster | | Env + validation | 763K ops/sec | 0.20x | 4.2x faster | 22x faster | | File + nested | 74K ops/sec | 0.70x | 2.2x faster | - |

Key points:

  • Fastest for simple env-only loading (1.7x faster than envalid)
  • Multi-source resolution adds overhead vs single-source libs
  • 74K ops/sec = ~13μs per resolve - plenty fast for startup config

See benchmark/ for full comparison.

License

MIT