Skip to content

config-kit overview

config-kit loads configuration from one or more sources, merges them in order, validates the result against a schema, and gives you back a typed object. Wrong env var? Build fails at boot, not at request time.

import { z } from "zod";
import {
loadConfig,
dotenvFileSource,
processEnvSource,
} from "@arshad-shah/config-kit";
const config = await loadConfig({
schema: z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
}),
sources: [
dotenvFileSource(".env"),
dotenvFileSource(".env.local"),
processEnvSource(),
],
});
config.PORT; // number
config.DATABASE_URL; // string, validated as URL

config is fully typed via z.infer<typeof schema>. There’s no shadow type definition to keep in sync.

  • Schema-agnostic - works with Zod, Valibot, ArkType, or anything with a parse(input) → output method
  • Flat or structured sources - flat env maps and nested, module-based config files (app.config.ts); see Module-based config
  • Layered sources - load from multiple places; later sources override earlier ones (flat sources merge key-by-key, structured sources deep-merge)
  • Soft source failures - a missing .env, config file, or unreachable remote endpoint doesn’t crash the load
  • Strict or warn - throw on invalid config (default) or downgrade to a logged warning with mode: "warn"
  • Host-controlled errors - onValidationError hands you the raw validation error (e.g. a ZodError) to render your own message
  • Secret-safe errors - validation errors redact quoted values by default so secrets don’t end up in logs
  • Optional logger - pass any object with info/warn/error methods (compatible with log-kit) for source-load diagnostics
  • No automatic file-watching/reload (config is captured at boot; for hot reload, build it yourself)
  • No built-in TypeScript compiler for config files - configFileSource takes a load hook so you bring esbuild/jiti (or rely on native import for .js/.mjs/.cjs/.json)
  • No defaults beyond objectSource and your schema’s defaults (Zod has .default(), Valibot has optional() with a fallback)

If you have a logger, pass it in:

import { createLogger } from "@arshad-shah/log-kit";
const log = createLogger();
const config = await loadConfig({ schema, sources, logger: log });

The logger gets info for each successful source load and warn if a source throws. No hard coupling - any object with the right shape works.