Search docs

Jump between documentation pages.

Browse docs

Config validation

Think of it like… a pre-flight checklist. Instead of taking off and discovering the fuel gauge is broken at 30,000 feet, you catch every problem on the ground — and you get the whole list at once, not one redeploy at a time.

defineConfig() is a single boot-time helper that loads your application configuration from a source you choose, validates the merged object against a Standard Schema validator (Zod, Valibot, ArkType, TypeBox, and others), and aggregates every validation issue into one structured error printed to stderr before the process exits.

The point is to fail fast and loud: a misconfigured deployment should surface every missing or invalid key in one shot, so operators do not have to redeploy four times to discover four different typos.

Quick start (from the environment)

By default defineConfig() reads from process.env. The result is fully typed from your schema.

ts
import { z } from "zod";
import { defineConfig } from "@daloyjs/core";

const Config = z.object({
  PORT: z.coerce.number().int().min(1).max(65535),
  DATABASE_URL: z.string().url(),
  NODE_ENV: z.enum(["development", "production", "test"]),
});

// Top-level await at module scope; resolves only when validation passed.
export const config = await defineConfig({ schema: Config });

// config.PORT is a number, config.DATABASE_URL is a string, etc.

If any key is missing or malformed, the process prints a summary and throws before your server ever binds a port:

text
defineConfig(): configuration is invalid (2 issues)
  - PORT: Expected number, received nan
  - DATABASE_URL: Invalid url

Choosing a source

The source option selects where the raw object comes from. The built-in sources are intentionally narrow; anything more elaborate (Vault, Doppler, AWS Secrets Manager) arrives through the custom source with an async resolver.

ts
// Default: read from process.env
await defineConfig({ schema: Config });
await defineConfig({ schema: Config, source: "env" });

// Read from an explicit env map (handy in tests)
await defineConfig({ schema: Config, source: { kind: "env", env: customEnv } });

// Read and parse a file on disk (defaults to JSON.parse)
await defineConfig({
  schema: Config,
  source: { kind: "file", path: "./config.json" },
});

// Validate an in-memory object
await defineConfig({
  schema: Config,
  source: { kind: "object", data: { PORT: "3000" } },
});

// Pull from an async secrets resolver
await defineConfig({
  schema: Config,
  source: { kind: "custom", resolve: () => fetchSecretsFromVault() },
});

Transforming before validation

Use transform to coerce or rename raw values before they hit the schema — for example mapping FOO_BAR to fooBar, or normalizing string flags. It receives the raw source object and returns the object handed to the validator.

ts
await defineConfig({
  schema: Config,
  transform: (raw) => ({
    ...raw,
    FEATURE_FLAGS: String(raw.FEATURE_FLAGS ?? "").split(","),
  }),
});

Handling the error programmatically

On failure, defineConfig() throws a ConfigValidationError whose issues array holds every { key, message } pair. Catch it when you want to render the failures in a startup probe or dashboard instead of relying on the stderr summary.

ts
import { defineConfig, ConfigValidationError } from "@daloyjs/core";

try {
  const config = await defineConfig({ schema: Config });
  startServer(config);
} catch (err) {
  if (err instanceof ConfigValidationError) {
    for (const issue of err.issues) {
      reportToHealthDashboard(issue.key, issue.message);
    }
  }
  throw err;
}

The stderr summary is on by default. Set stderr: false to suppress the printed output; the thrown ConfigValidationError still carries issues.