Конфигурационные файлы

Конфигурационные файлы в современных JavaScript-приложениях выполняют роль централизованного источника параметров, от которых зависит поведение системы в разных окружениях: разработка, тестирование, продакшн. Типичные проблемы возникают из-за отсутствия строгой типизации и валидации: переменные окружения приходят строками, JSON-файлы могут содержать неполные или некорректные данные, а ошибки проявляются уже в рантайме. Использование Zod позволяет перевести конфигурацию в строго проверяемую структуру с гарантированной корректностью на этапе запуска приложения.

Основной подход заключается в описании схемы конфигурации как объекта, где каждое поле явно типизировано и проверяется:

import { z } from "zod";

const configSchema = z.object({
  port: z.number(),
  host: z.string(),
  nodeEnv: z.enum(["development", "test", "production"]),
});

Такое описание уже задаёт контракт: приложение не может стартовать с некорректной конфигурацией. Однако реальные источники данных (например, process.env) требуют дополнительной обработки, поскольку всегда возвращают строки или undefined.

Преобразование переменных окружения

Переменные окружения необходимо приводить к нужным типам:

const envSchema = z.object({
  PORT: z.string().transform((val) => Number(val)),
  HOST: z.string(),
  NODE_ENV: z.enum(["development", "test", "production"]),
});

Более устойчивый вариант включает проверку числовых значений:

const envSchema = z.object({
  PORT: z.coerce.number(),
  HOST: z.string(),
  NODE_ENV: z.enum(["development", "test", "production"]),
});

Использование z.coerce устраняет необходимость ручного преобразования и снижает вероятность ошибок при парсинге.

Конфигурационный модуль как единая точка доступа

Практика выделения отдельного модуля конфигурации позволяет централизовать валидацию:

const env = envSchema.parse(process.env);

export const config = {
  port: env.PORT,
  host: env.HOST,
  nodeEnv: env.NODE_ENV,
};

Использование parse гарантирует остановку приложения при некорректной конфигурации. В более устойчивых системах применяется safeParse:

const result = envSchema.safeParse(process.env);

if (!result.success) {
  console.error(result.error.format());
  process.exit(1);
}

export const config = result.data;

Такой подход позволяет контролировать формат ошибок и интегрировать логирование.

Значения по умолчанию

Конфигурация редко полностью определяется окружением. Часто требуется задание дефолтов:

const configSchema = z.object({
  port: z.coerce.number().default(3000),
  host: z.string().default("localhost"),
});

Механизм default применяется только при undefined, но не при пустых строках, что важно учитывать при работе с окружением.

Для более гибких сценариев используется catch:

port: z.coerce.number().catch(3000),

Чтение конфигурации из JSON-файлов

Помимо переменных окружения, конфигурации часто хранятся в JSON:

import fs from "fs";

const rawConfig = JSON.parse(
  fs.readFileSync("./config.json", "utf-8")
);

Схема Zod применяется аналогично:

const fileConfigSchema = z.object({
  port: z.number(),
  databaseUrl: z.string().url(),
});
const config = fileConfigSchema.parse(rawConfig);

При этом схема остаётся единственным источником истины для структуры данных.

Слияние конфигураций из нескольких источников

Типичный сценарий — объединение базового файла и окружения:

const baseConfig = fileConfigSchema.parse(
  JSON.parse(fs.readFileSync("./config.base.json", "utf-8"))
);

const envConfig = envSchema.parse(process.env);

Далее выполняется объединение:

export const config = {
  ...baseConfig,
  port: envConfig.PORT,
};

Zod также позволяет описывать итоговую структуру напрямую:

const finalConfigSchema = baseConfigSchema.merge(
  z.object({
    port: z.coerce.number(),
  })
);

Вложенные конфигурации

Сложные приложения используют структурированные конфигурации:

const configSchema = z.object({
  server: z.object({
    port: z.coerce.number(),
    host: z.string(),
  }),
  db: z.object({
    url: z.string(),
    poolSize: z.number().default(10),
  }),
});

Такой подход повышает читаемость и предотвращает «плоские» конфигурации, которые сложно масштабировать.

Перечисления и ограниченные значения

Для режимов работы или стратегий используются enum-схемы:

const logLevel = z.enum(["debug", "info", "warn", "error"]);

или более гибко:

const logLevel = z.union([
  z.literal("debug"),
  z.literal("info"),
  z.literal("warn"),
  z.literal("error"),
]);

Уточнение ограничений через refinement

Некоторые параметры требуют бизнес-ограничений:

const port = z.coerce
  .number()
  .int()
  .min(1)
  .max(65535);

Более сложные проверки выполняются через refine:

const databaseUrl = z.string().refine((val) => val.startsWith("postgres://"), {
  message: "URL должен быть PostgreSQL",
});

Типизация конфигурации

Одним из ключевых преимуществ Zod является автоматическое получение TypeScript-типа:

type Config = z.infer<typeof configSchema>;

Это обеспечивает синхронизацию runtime-валидации и статической типизации без дублирования описаний.

Отложенная валидация и ленивые схемы

При сложной конфигурации возможны циклические зависимости схем:

const schema = z.lazy(() =>
  z.object({
    next: schema.optional(),
  })
);

Подобный механизм используется редко, но полезен для рекурсивных структур конфигурации.

Стратегии обработки ошибок конфигурации

Ошибки Zod содержат детальную структуру, позволяющую формировать человекочитаемые сообщения или машинно-обрабатываемые отчёты:

const result = configSchema.safeParse(data);

if (!result.success) {
  const formatted = result.error.format();
}

В крупных системах ошибка конфигурации часто агрегируется и выводится единым отчётом перед остановкой процесса.

Изоляция конфигурационного слоя

Практика разделения ответственности предполагает выделение отдельного слоя:

  • чтение источников (env, json, yaml)
  • нормализация данных
  • валидация через Zod
  • экспорт неизменяемого объекта
const config = Object.freeze(parsedConfig);

Это предотвращает случайные изменения в рантайме.

Композиция схем

Повторно используемые части конфигурации выделяются отдельно:

const databaseSchema = z.object({
  url: z.string(),
  poolSize: z.number(),
});

const configSchema = z.object({
  db: databaseSchema,
  cache: databaseSchema.partial(),
});

Композиция уменьшает дублирование и упрощает поддержку.

Строгий режим и лишние поля

Zod позволяет контролировать избыточные данные:

const schema = z.object({
  port: z.number(),
}).strict();

При наличии дополнительных полей в конфигурации будет выброшена ошибка, что полезно для предотвращения «тихих» опечаток.

Динамическая сборка конфигурации

В некоторых случаях схема зависит от окружения:

const isProd = process.env.NODE_ENV === "production";

const schema = z.object({
  debug: isProd ? z.literal(false) : z.boolean(),
});

Это позволяет адаптировать структуру конфигурации под контекст запуска без изменения логики приложения.