Конфигурационные файлы в современных 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:
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"),
]);
Некоторые параметры требуют бизнес-ограничений:
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();
}
В крупных системах ошибка конфигурации часто агрегируется и выводится единым отчётом перед остановкой процесса.
Практика разделения ответственности предполагает выделение отдельного слоя:
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(),
});
Это позволяет адаптировать структуру конфигурации под контекст запуска без изменения логики приложения.