Skip to content
intermediate Phase 10 · TypeScript Schema Validation

Environment & Config Validation

Validate environment variables and config objects at startup.

45m
0 problems
Topic Progress 0%

Why Startup Validation

Why Startup Validation

Environment variables are untyped strings that may be absent, empty, or wrong:

const port = Number(process.env.PORT);   // NaN if unset
fetch(process.env.DATABASE_URL);         // "undefined" in a connection string

A missing variable should crash the process at boot with a message naming every problem — not hours later inside a rarely hit branch:

// config/env.ts — only module that reads process.env
export const env = loadEnv();            // throws a readable report on failure
export type Env = typeof env;            // inferred — no parallel interface

Type the shape for editors:

declare global {
  namespace NodeJS {
    interface ProcessEnv { NODE_ENV: string; DATABASE_URL: string }
  }
}

ProcessEnv typing serves only the config module — the app receives the validated object, never process.env.

A Concrete Zod Config Loader

A Concrete Zod Config Loader

import { z } from "zod";
import "dotenv/config";   // must run before parse

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  DATABASE_URL: z.string().url(),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});

const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error("Invalid environment:", parsed.error.issues
    .map((i) => `  ${i.path.join(".")}: ${i.message}`).join("\n"));
  process.exit(1);            // fail fast
}

export const env = Object.freeze(parsed.data);

Coercion handles PORT="8080"; the enum rejects LOG_LEVEL=verbose; defaults keep local dev light while production must supply DATABASE_URL. Object.freeze makes config read-only after boot.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

  • Import order: load dotenv/config before the schema parse — later loads will not refresh values other modules already read.
  • Lazy re-reads: validating on every use reintroduces the original bug — one eager load, one frozen object.
  • Secrets hygiene: .env stays in .gitignore; real secrets come from the platform’s store. Validate shape in CI against .env.example.
  • Injection: pass env into constructors — new Server(env.PORT) — so tests construct {...env, PORT: 0} fakes without monkeypatching process.env.
  • Environment differences: one schema, per-environment values; use superRefine with NODE_ENV for prod-only requirements.

Interview angle: describe boot order (dotenv → schema parse → freeze → export), the failure output format, and why process.exit(1) beats throwing here (clear ops signal).

Quiz

1. When should process.env be validated?

Question 1 options

2. Why z.coerce.number() for PORT?

Question 2 options

3. Why export a frozen config object instead of reading process.env everywhere?

Question 3 options

Flashcards

Question

Fail-fast config

Answer

Parse and validate env at startup; exit(1) with issue list rather than failing mid-request.

Question

dotenv ordering

Answer

Load .env before schema validation — later loads will not refresh already-read values.

Question

z.coerce for env

Answer

Strings become numbers/booleans before validation ranges and enums apply.

Question

Config injection

Answer

Pass the frozen env object into constructors — tests substitute fakes without patching globals.

Revision Notes

Key Takeaways

  • 1. Env vars are untyped strings; validate shape once, at boot, and freeze the result
  • 2. Enums, defaults, and coercion encode operational rules in the schema itself
  • 3. Inject config — never scatter process.env reads through domain code

Interview Tips

  • • Walk the exact boot sequence and show the human-readable error format you would print
  • • Explain how CI proves the sample env parses without exposing real secrets

Cheat Sheet

dotenv → EnvSchema.safeParse(process.env) → issues list → exit(1) → Object.freeze; z.coerce PORT, z.enum LOG_LEVEL; inject env object, never process.env.

Continue Learning