env-validation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBuild-Time Environment Variable Validation
构建时环境变量验证
Validate environment variables on server start and before builds. Catch missing or invalid variables early with clear error messages.
在服务器启动时和构建前验证环境变量,通过清晰的错误消息提前捕获缺失或无效的变量。
Implement Environment Validation
实现环境变量验证
Validate environment variables on server start and before builds. Catch missing or invalid variables early with clear error messages.
See:
- Resource: in Fullstack Recipes
env-validation - URL: https://fullstackrecipes.com/recipes/env-validation
在服务器启动时和构建前验证环境变量,通过清晰的错误消息提前捕获缺失或无效的变量。
参考:
- 资源:Fullstack Recipes中的
env-validation - 链接:https://fullstackrecipes.com/recipes/env-validation
Validating Configs on Server Start
服务器启动时验证配置
Some environment variables are read internally by packages rather than passed as arguments. To catch missing variables early instead of at runtime, import your configs in :
instrumentation.tstypescript
// src/instrumentation.ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./lib/sentry/config";
// Validate required configs on server start
import "./lib/ai/config";
import "./lib/db/config";
export async function register() {
// ... initialization code
}The side-effect imports trigger validation immediately when the server starts. If any required environment variable is missing, the server fails to start with a clear error rather than failing later when the code path is executed.
configSchema部分环境变量由包内部读取,而非作为参数传入。为了在运行前提前捕获缺失的变量,可在中导入你的配置:
instrumentation.tstypescript
// src/instrumentation.ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./lib/sentry/config";
// Validate required configs on server start
import "./lib/ai/config";
import "./lib/db/config";
export async function register() {
// ... initialization code
}这种副作用导入会在服务器启动时立即触发验证。如果任何必需的环境变量缺失,服务器会无法启动并显示清晰的错误,而非在后续执行代码路径时才失败。
configSchemaValidating Environment Files Pre-Build
构建前验证环境文件
Install via shadcn registry:
bash
bunx --bun shadcn@latest add https://fullstackrecipes.com/r/validate-env.jsonOr copy the source code:
scripts/validate-env.tstypescript
#!/usr/bin/env bun
/**
* Validate environment configuration
*
* Usage:
* bun run validate-env
* bun run validate-env --environment=development
* bun run validate-env --environment=production
*
* This script:
* 1. Loads env files using Next.js's loadEnvConfig
* 2. Finds all config.ts files in src/lib/\*\/
* 3. Validates each config by importing it (triggers configSchema validation)
* 4. Warns about env variables in .env files that aren't used by any config
*/
import { loadEnvConfig } from "@next/env";
import { Glob } from "bun";
import path from "path";
// ANSI colors
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
// Parse CLI args
function parseArgs(): { environment?: string } {
const args = process.argv.slice(2);
const result: { environment?: string } = {};
for (const arg of args) {
if (arg.startsWith("--environment=")) {
result.environment = arg.split("=")[1];
}
}
return result;
}
// Track which env vars are referenced by configs
const referencedEnvVars = new Set<string>();
// Patch process.env to track access
function trackEnvAccess() {
const originalEnv = process.env;
const handler: ProxyHandler<NodeJS.ProcessEnv> = {
get(target, prop) {
if (typeof prop === "string" && !prop.startsWith("_")) {
referencedEnvVars.add(prop);
}
return Reflect.get(target, prop);
},
};
process.env = new Proxy(originalEnv, handler);
}
async function main() {
const args = parseArgs();
const projectDir = process.cwd();
console.log(bold("\n🔍 Environment Configuration Validator\n"));
// Set NODE_ENV if environment specified
const environment = args.environment ?? process.env.NODE_ENV ?? "development";
(process.env as Record<string, string>).NODE_ENV = environment;
console.log(dim(` Environment: ${environment}\n`));
// Load env files
// Second param `dev` tells loadEnvConfig to load .env.development files
const isDev = environment === "development";
console.log(dim(" Loading environment files..."));
const loadedEnvFiles: string[] = [];
const { combinedEnv, loadedEnvFiles: files } = loadEnvConfig(
projectDir,
isDev,
);
for (const file of files) {
loadedEnvFiles.push(file.path);
console.log(dim(` ✓ ${path.relative(projectDir, file.path)}`));
}
if (loadedEnvFiles.length === 0) {
console.log(dim(" No .env files found"));
}
console.log("");
// Start tracking env access before importing configs
trackEnvAccess();
// Find all config.ts files
const configGlob = new Glob("src/lib/*/config.ts");
const configFiles: string[] = [];
for await (const file of configGlob.scan(projectDir)) {
configFiles.push(file);
}
if (configFiles.length === 0) {
console.log(yellow(" ⚠ No config.ts files found in src/lib/*/\n"));
process.exit(0);
}
console.log(dim(` Found ${configFiles.length} config files:\n`));
// Validate each config
const errors: { file: string; error: Error }[] = [];
const validated: string[] = [];
for (const configFile of configFiles) {
const relativePath = configFile;
const absolutePath = path.join(projectDir, configFile);
try {
// Import the config module - this triggers validation
await import(absolutePath);
console.log(green(` ✓ ${relativePath}`));
validated.push(relativePath);
} catch (error) {
if (error instanceof Error) {
// Check if it's a disabled feature flag (not an error)
if (error.message.includes("isEnabled: false")) {
console.log(dim(` ○ ${relativePath} (feature disabled)`));
validated.push(relativePath);
} else {
console.log(red(` ✗ ${relativePath}`));
errors.push({ file: relativePath, error });
}
}
}
}
console.log("");
// Report validation errors
if (errors.length > 0) {
console.log(red(bold("Validation Errors:\n")));
for (const { file, error } of errors) {
console.log(red(` ${file}:`));
// Extract the actual error message
const message = error.message.split("\n").slice(0, 3).join("\n ");
console.log(red(` ${message}\n`));
}
}
// Find unused env variables (in .env files but not referenced by configs)
const envVarsInFiles = new Set<string>();
// Parse loaded env files to get all defined variables
for (const envFile of loadedEnvFiles) {
try {
const content = await Bun.file(envFile).text();
const lines = content.split("\n");
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith("#")) continue;
// Extract variable name (before = sign)
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
if (match) {
envVarsInFiles.add(match[1]);
}
}
} catch {
// Ignore file read errors
}
}
// Common system/framework vars to ignore
const ignoredVars = new Set([
// System
"NODE_ENV",
"PATH",
"HOME",
"USER",
"SHELL",
"TERM",
"LANG",
"PWD",
"OLDPWD",
"HOSTNAME",
"LOGNAME",
"TMPDIR",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_CACHE_HOME",
"CI",
"TZ",
// Vercel
"VERCEL",
"VERCEL_ENV",
"VERCEL_URL",
"VERCEL_REGION",
"VERCEL_TARGET_ENV",
"VERCEL_GIT_COMMIT_SHA",
"VERCEL_GIT_COMMIT_MESSAGE",
"VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
"VERCEL_GIT_COMMIT_AUTHOR_NAME",
"VERCEL_GIT_PREVIOUS_SHA",
"VERCEL_GIT_PROVIDER",
"VERCEL_GIT_REPO_ID",
"VERCEL_GIT_REPO_OWNER",
"VERCEL_GIT_REPO_SLUG",
"VERCEL_GIT_COMMIT_REF",
"VERCEL_GIT_PULL_REQUEST_ID",
// Build tools (Turbo, NX)
"TURBO_CACHE",
"TURBO_REMOTE_ONLY",
"TURBO_RUN_SUMMARY",
"TURBO_DOWNLOAD_LOCAL_ENABLED",
"NX_DAEMON",
]);
// Find vars in .env files but not referenced by configs
const unusedVars: { name: string; files: string[] }[] = [];
for (const envVar of envVarsInFiles) {
if (ignoredVars.has(envVar)) continue;
if (referencedEnvVars.has(envVar)) continue;
// Find which files define this var
const definingFiles: string[] = [];
for (const envFile of loadedEnvFiles) {
try {
const content = await Bun.file(envFile).text();
if (new RegExp(`^${envVar}\\s*=`, "m").test(content)) {
definingFiles.push(path.relative(projectDir, envFile));
}
} catch {
// Ignore
}
}
if (definingFiles.length > 0) {
unusedVars.push({ name: envVar, files: definingFiles });
}
}
// Report unused vars
if (unusedVars.length > 0) {
console.log(yellow(bold("Unused Environment Variables:\n")));
console.log(
dim(
" These variables are defined in .env files but not used by any config:\n",
),
);
for (const { name, files } of unusedVars.sort((a, b) =>
a.name.localeCompare(b.name),
)) {
console.log(yellow(` ⚠ ${name}`));
console.log(dim(` defined in: ${files.join(", ")}`));
}
console.log("");
}
// Summary
console.log(bold("Summary:\n"));
console.log(` Configs validated: ${green(String(validated.length))}`);
console.log(
` Validation errors: ${errors.length > 0 ? red(String(errors.length)) : green("0")}`,
);
console.log(
` Unused env vars: ${unusedVars.length > 0 ? yellow(String(unusedVars.length)) : green("0")}`,
);
console.log("");
// Exit with error code if validation failed
if (errors.length > 0) {
process.exit(1);
}
}
main().catch((error) => {
console.error(red("Unexpected error:"), error);
process.exit(1);
});Add the validation script to your :
package.jsonjson
{
"scripts": {
"prebuild": "bun run env:validate:prod",
"env:validate": "bun run scripts/validate-env.ts --environment=development",
"env:validate:prod": "bun run scripts/validate-env.ts --environment=production"
}
}Use the and scripts to validate all your configs ( files in ) against your files.
env:validateenv:validate:prodconfig.tssrc/lib/*/.envThe script (configured above) runs automatically before , ensuring environment variables are validated before every build (locally and in CI/Vercel). If validation fails, the build stops early with a clear error.
prebuildbuildThe script:
- Loads files using Next.js's
.env(respects the same load order as Next.js)loadEnvConfig - Finds all files in
config.tssrc/lib/*/ - Imports each config to trigger validation
configSchema - Reports any missing or invalid environment variables
- Warns about variables defined in files but not used by any config
.env
Example output with a validation error:
🔍 Environment Configuration Validator
Environment: development
Loading environment files...
✓ .env.local
✓ .env.development
Found 5 config files:
✗ src/lib/resend/config.ts
✓ src/lib/sentry/config.ts
✓ src/lib/db/config.ts
✓ src/lib/ai/config.ts
✓ src/lib/auth/config.ts
Validation Errors:
src/lib/resend/config.ts:
Configuration validation error for Resend!
Did you correctly set all required environment variables in your .env* file?
- server.fromEmail (FROM_EMAIL) must be defined.
Summary:
Configs validated: 4
Validation errors: 1
Unused env vars: 0Example output with an unused variable:
🔍 Environment Configuration Validator
Environment: development
Loading environment files...
✓ .env.local
✓ .env.development
Found 5 config files:
✓ src/lib/resend/config.ts
✓ src/lib/sentry/config.ts
✓ src/lib/db/config.ts
✓ src/lib/ai/config.ts
✓ src/lib/auth/config.ts
Unused Environment Variables:
These variables are defined in .env files but not used by any config:
⚠ OLD_API_KEY
defined in: .env.local
Summary:
Configs validated: 5
Validation errors: 0
Unused env vars: 1The script exits with code 1 if any validation errors occur (useful for CI), but unused variables only trigger warnings without failing the build.
通过shadcn注册表安装:
bash
bunx --bun shadcn@latest add https://fullstackrecipes.com/r/validate-env.json或者复制源代码:
scripts/validate-env.tstypescript
#!/usr/bin/env bun
/**
* Validate environment configuration
*
* Usage:
* bun run validate-env
* bun run validate-env --environment=development
* bun run validate-env --environment=production
*
* This script:
* 1. Loads env files using Next.js's loadEnvConfig
* 2. Finds all config.ts files in src/lib/\*\/
* 3. Validates each config by importing it (triggers configSchema validation)
* 4. Warns about env variables in .env files that aren't used by any config
*/
import { loadEnvConfig } from "@next/env";
import { Glob } from "bun";
import path from "path";
// ANSI colors
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
// Parse CLI args
function parseArgs(): { environment?: string } {
const args = process.argv.slice(2);
const result: { environment?: string } = {};
for (const arg of args) {
if (arg.startsWith("--environment=")) {
result.environment = arg.split("=")[1];
}
}
return result;
}
// Track which env vars are referenced by configs
const referencedEnvVars = new Set<string>();
// Patch process.env to track access
function trackEnvAccess() {
const originalEnv = process.env;
const handler: ProxyHandler<NodeJS.ProcessEnv> = {
get(target, prop) {
if (typeof prop === "string" && !prop.startsWith("_")) {
referencedEnvVars.add(prop);
}
return Reflect.get(target, prop);
},
};
process.env = new Proxy(originalEnv, handler);
}
async function main() {
const args = parseArgs();
const projectDir = process.cwd();
console.log(bold("\n🔍 Environment Configuration Validator\n"));
// Set NODE_ENV if environment specified
const environment = args.environment ?? process.env.NODE_ENV ?? "development";
(process.env as Record<string, string>).NODE_ENV = environment;
console.log(dim(` Environment: ${environment}\n`));
// Load env files
// Second param `dev` tells loadEnvConfig to load .env.development files
const isDev = environment === "development";
console.log(dim(" Loading environment files..."));
const loadedEnvFiles: string[] = [];
const { combinedEnv, loadedEnvFiles: files } = loadEnvConfig(
projectDir,
isDev,
);
for (const file of files) {
loadedEnvFiles.push(file.path);
console.log(dim(` ✓ ${path.relative(projectDir, file.path)}`));
}
if (loadedEnvFiles.length === 0) {
console.log(dim(" No .env files found"));
}
console.log("");
// Start tracking env access before importing configs
trackEnvAccess();
// Find all config.ts files
const configGlob = new Glob("src/lib/*/config.ts");
const configFiles: string[] = [];
for await (const file of configGlob.scan(projectDir)) {
configFiles.push(file);
}
if (configFiles.length === 0) {
console.log(yellow(" ⚠ No config.ts files found in src/lib/*/\n"));
process.exit(0);
}
console.log(dim(` Found ${configFiles.length} config files:\n`));
// Validate each config
const errors: { file: string; error: Error }[] = [];
const validated: string[] = [];
for (const configFile of configFiles) {
const relativePath = configFile;
const absolutePath = path.join(projectDir, configFile);
try {
// Import the config module - this triggers validation
await import(absolutePath);
console.log(green(` ✓ ${relativePath}`));
validated.push(relativePath);
} catch (error) {
if (error instanceof Error) {
// Check if it's a disabled feature flag (not an error)
if (error.message.includes("isEnabled: false")) {
console.log(dim(` ○ ${relativePath} (feature disabled)`));
validated.push(relativePath);
} else {
console.log(red(` ✗ ${relativePath}`));
errors.push({ file: relativePath, error });
}
}
}
}
console.log("");
// Report validation errors
if (errors.length > 0) {
console.log(red(bold("Validation Errors:\n")));
for (const { file, error } of errors) {
console.log(red(` ${file}:`));
// Extract the actual error message
const message = error.message.split("\n").slice(0, 3).join("\n ");
console.log(red(` ${message}\n`));
}
}
// Find unused env variables (in .env files but not referenced by configs)
const envVarsInFiles = new Set<string>();
// Parse loaded env files to get all defined variables
for (const envFile of loadedEnvFiles) {
try {
const content = await Bun.file(envFile).text();
const lines = content.split("\n");
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith("#")) continue;
// Extract variable name (before = sign)
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
if (match) {
envVarsInFiles.add(match[1]);
}
}
} catch {
// Ignore file read errors
}
}
// Common system/framework vars to ignore
const ignoredVars = new Set([
// System
"NODE_ENV",
"PATH",
"HOME",
"USER",
"SHELL",
"TERM",
"LANG",
"PWD",
"OLDPWD",
"HOSTNAME",
"LOGNAME",
"TMPDIR",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_CACHE_HOME",
"CI",
"TZ",
// Vercel
"VERCEL",
"VERCEL_ENV",
"VERCEL_URL",
"VERCEL_REGION",
"VERCEL_TARGET_ENV",
"VERCEL_GIT_COMMIT_SHA",
"VERCEL_GIT_COMMIT_MESSAGE",
"VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
"VERCEL_GIT_COMMIT_AUTHOR_NAME",
"VERCEL_GIT_PREVIOUS_SHA",
"VERCEL_GIT_PROVIDER",
"VERCEL_GIT_REPO_ID",
"VERCEL_GIT_REPO_OWNER",
"VERCEL_GIT_REPO_SLUG",
"VERCEL_GIT_COMMIT_REF",
"VERCEL_GIT_PULL_REQUEST_ID",
// Build tools (Turbo, NX)
"TURBO_CACHE",
"TURBO_REMOTE_ONLY",
"TURBO_RUN_SUMMARY",
"TURBO_DOWNLOAD_LOCAL_ENABLED",
"NX_DAEMON",
]);
// Find vars in .env files but not referenced by configs
const unusedVars: { name: string; files: string[] }[] = [];
for (const envVar of envVarsInFiles) {
if (ignoredVars.has(envVar)) continue;
if (referencedEnvVars.has(envVar)) continue;
// Find which files define this var
const definingFiles: string[] = [];
for (const envFile of loadedEnvFiles) {
try {
const content = await Bun.file(envFile).text();
if (new RegExp(`^${envVar}\\s*=`, "m").test(content)) {
definingFiles.push(path.relative(projectDir, envFile));
}
} catch {
// Ignore
}
}
if (definingFiles.length > 0) {
unusedVars.push({ name: envVar, files: definingFiles });
}
}
// Report unused vars
if (unusedVars.length > 0) {
console.log(yellow(bold("Unused Environment Variables:\n")));
console.log(
dim(
" These variables are defined in .env files but not used by any config:\n",
),
);
for (const { name, files } of unusedVars.sort((a, b) =>
a.name.localeCompare(b.name),
)) {
console.log(yellow(` ⚠ ${name}`));
console.log(dim(` defined in: ${files.join(", ")}`));
}
console.log("");
}
// Summary
console.log(bold("Summary:\n"));
console.log(` Configs validated: ${green(String(validated.length))}`);
console.log(
` Validation errors: ${errors.length > 0 ? red(String(errors.length)) : green("0")}`,
);
console.log(
` Unused env vars: ${unusedVars.length > 0 ? yellow(String(unusedVars.length)) : green("0")}`,
);
console.log("");
// Exit with error code if validation failed
if (errors.length > 0) {
process.exit(1);
}
}
main().catch((error) => {
console.error(red("Unexpected error:"), error);
process.exit(1);
});将验证脚本添加到你的中:
package.jsonjson
{
"scripts": {
"prebuild": "bun run env:validate:prod",
"env:validate": "bun run scripts/validate-env.ts --environment=development",
"env:validate:prod": "bun run scripts/validate-env.ts --environment=production"
}
}使用和脚本,针对你的文件验证所有配置(目录下的文件)。
env:validateenv:validate:prod.envsrc/lib/*/config.ts上述配置的脚本会在命令前自动运行,确保每次构建(本地和CI/Vercel中)前都会验证环境变量。如果验证失败,构建会提前终止并显示清晰的错误。
prebuildbuild该脚本具备以下功能:
- 使用Next.js的加载
loadEnvConfig文件(遵循与Next.js相同的加载顺序).env - 查找目录下所有的
src/lib/*/文件config.ts - 导入每个配置以触发验证
configSchema - 报告任何缺失或无效的环境变量
- 警告文件中定义但未被任何配置使用的变量
.env
验证错误的示例输出:
🔍 Environment Configuration Validator
Environment: development
Loading environment files...
✓ .env.local
✓ .env.development
Found 5 config files:
✗ src/lib/resend/config.ts
✓ src/lib/sentry/config.ts
✓ src/lib/db/config.ts
✓ src/lib/ai/config.ts
✓ src/lib/auth/config.ts
Validation Errors:
src/lib/resend/config.ts:
Configuration validation error for Resend!
Did you correctly set all required environment variables in your .env* file?
- server.fromEmail (FROM_EMAIL) must be defined.
Summary:
Configs validated: 4
Validation errors: 1
Unused env vars: 0存在未使用变量的示例输出:
🔍 Environment Configuration Validator
Environment: development
Loading environment files...
✓ .env.local
✓ .env.development
Found 5 config files:
✓ src/lib/resend/config.ts
✓ src/lib/sentry/config.ts
✓ src/lib/db/config.ts
✓ src/lib/ai/config.ts
✓ src/lib/auth/config.ts
Unused Environment Variables:
These variables are defined in .env files but not used by any config:
⚠ OLD_API_KEY
defined in: .env.local
Summary:
Configs validated: 5
Validation errors: 0
Unused env vars: 1如果出现任何验证错误,脚本会以错误码1退出(适用于CI环境),但未使用变量仅会触发警告,不会导致构建失败。