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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@devscast/config

v1.1.1

Published

Helps you find, load, combine, autofill and validate configuration values of any kind

Readme

Config : Typesafe configuration loader

npm npm Lint Tests GitHub


Overview

@devscast/config provides a batteries-included configuration loader for Node.js projects. It lets you:

  • Load configuration from JSON, YAML, INI, or inline objects defined in code
  • Reference environment variables in text files with the %env(FOO)% syntax
  • Bootstrap environment values from .env files (including .env.local, .env.<env>, .env.<env>.local)
  • Validate the resulting configuration with a Zod v4 schema before your app starts
  • Use the typed env() helper throughout your app for safe access to process.env

Installation

npm install @devscast/config zod

@devscast/config treats Zod v4 as a required peer dependency, so make sure it is present in your project. This package imports from zod/mini internally to keep bundles lean. If your schemas only rely on the features exposed by the mini build (objects, strings, numbers, enums, unions, coercion, effects, etc.), consider importing z from zod/mini in your own code as well for consistent tree-shaking. Need YAML or INI parsing? Install the optional peers alongside the core package:

npm install yaml ini

Example Usage

import path from "node:path";
import { z } from "zod/mini";
import { defineConfig } from "@devscast/config";

const schema = z.object({
  database: z.object({
    host: z.string(),
    port: z.coerce.number(),
    username: z.string(),
    password: z.string(),
  }),
  featureFlags: z.array(z.string()).default([]),
});

const { config, env } = defineConfig({
  schema,
  cwd: process.cwd(),
  env: { path: path.join(process.cwd(), ".env") },
  sources: [
    path.join("config", "default.yaml"),
    { path: path.join("config", `${env("NODE_ENV", { default: "dev" })}.yaml`), optional: true },
    { featureFlags: ["beta-search"] },
  ],
});

console.log(config.database.host);

Additional Use Cases

Combine multiple formats with fallbacks

import path from "node:path";
import { defineConfig } from "@devscast/config";

const { config, env } = defineConfig({
  schema,
  env: true,
  sources: [
    path.join("config", "base.json"),
    path.join("config", "defaults.yaml"),
    { path: path.join("secrets", "overrides.ini"), optional: true },
    { featureFlags: (env.optional("FEATURE_FLAGS") ?? "").split(",").filter(Boolean) },
  ],
});
  • String entries infer the format from the extension; optional INI/YAML support depends on the peer deps above.
  • Inline objects in sources are merged last, so they are useful for computed values or environment overrides.

Typed environment accessor for reusable helpers

import { defineConfig } from "@devscast/config";

const { config, env } = defineConfig({
  schema,
  env: {
    path: ".env",
    knownKeys: ["NODE_ENV", "DB_HOST", "DB_PORT"] as const,
  },
});

export function createDatabaseUrl() {
  return `postgres://${env("DB_HOST")}:${env("DB_PORT")}/app`;
}
  • Providing knownKeys narrows the env accessor typings, surfacing autocomplete within your app.
  • The accessor mirrors process.env but throws when a key is missing; switch to env.optional("DB_HOST") when the variable is truly optional.

Environment-only configuration (no external files)

import { z } from "zod/mini";
import { createEnvAccessor } from "@devscast/config";

const schema = z.object({
  appEnv: z.enum(["dev", "prod", "test"]).default("dev"),
  port: z.coerce.number().int().min(1).max(65535).default(3000),
  redisUrl: z.string().url(),
});

const env = createEnvAccessor(["NODE_ENV", "APP_PORT", "REDIS_URL"] as const);

const config = schema.parse({
  appEnv: env("NODE_ENV", { default: "dev" }),
  port: Number(env("APP_PORT", { default: "3000" })),
  redisUrl: env("REDIS_URL"),
});
  • createEnvAccessor gives you the same typed helper without invoking defineConfig, ideal for lightweight scripts.
  • You can still validate the derived values with Zod (or any other validator) before using them.

Inline configuration objects (no compiled files)

import { z } from "zod/mini";
import { defineConfig } from "@devscast/config";

const schema = z.object({
  database: z.object({
    host: z.string(),
    port: z.number(),
  }),
  featureFlags: z.preprocess(
    value => {
      if (typeof value === "string") {
        return value
          .split(",")
          .map(flag => flag.trim())
          .filter(Boolean);
      }
      return value;
    },
    z.array(z.string())
  ),
});

const { config } = defineConfig({
  schema,
  env: { path: ".env" },
  sources: {
    database: { host: "%env(DB_HOST)%", port: "%env(number:DB_PORT)%" },
    featureFlags: "%env(FEATURE_FLAGS)%",
  },
});

console.log(config.database);
  • Inline objects remove the need for TypeScript config files while still allowing env interpolation.
  • Typed placeholders resolve after .env files load; use Zod preprocessors for shapes like comma-delimited lists.

Referencing environment variables

  • Text-based configs (JSON, YAML, INI): use %env(DB_HOST)%
  • Typed placeholders: %env(number:PORT)%, %env(boolean:FEATURE)%, %env(string:NAME)%
    • When the entire value is a single placeholder, typed forms produce native values (number/boolean).
    • When used inside larger strings (e.g. "http://%env(API_HOST)%/v1"), placeholders are interpolated as text.
  • Inline objects: placeholders work the same way; combine them with Zod preprocessors for complex shapes (arrays, URLs, etc.).

The env() helper throws when the variable is missing. Provide a default with env("PORT", { default: "3000" }) or switch to env.optional("PORT").

Dotenv loading

defineConfig automatically understands .env files when the env option is provided. The resolver honours the following precedence, mirroring Symfony's Dotenv component:

  1. .env (or .env.dist when .env is missing)
  2. .env.local (skipped when NODE_ENV === "test")
  3. .env.<NODE_ENV>
  4. .env.<NODE_ENV>.local

Local files always win over base files. The loaded keys are registered on the shared env accessor so they show up in editor autocomplete once your editor reloads types.

Command expansion opt-in

Command substitution via $(...) is now opt-in for .env files. By default these sequences are kept as literal strings. To re-enable shell execution, add a directive comment at the top of the file:

# @dotenv-expand-commands
SECRET_KEY=$(openssl rand -hex 32)

Once the tag is present, all subsequent entries can use command expansion; omitting it keeps parsing side-effect free. If a command exits with a non-zero status or otherwise fails, the parser now keeps the original $(...) literal so .env loading continues without interruption.

Contributors