env-validation について
サーバー起動時とビルド前に環境変数を検証するFullstack Recipesのスキルです。設定の不足や不正を早期に見つけるため、設定検証とBunスクリプトをビルドのワークフローに組み込めます。
主な機能
- 01サーバー起動時の設定検証
- 02ビルド前の環境ファイル検証
- 03Next.jsの順序で環境ファイルを読み込み
- 04config.tsファイルを検出して検証
- 05未使用の変数を警告
- 06設定不正時にエラー終了
活用シーン
- 01サーバー起動時に設定不足を検出
- 02環境値が不正な場合にビルドを停止
- 03ローカルとCIのビルド設定を確認
- 04設定で使われていない環境変数を特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ andrelandgraf/ fullstackrecipes --skill env-validationインストール後、Claude Code で/env-validationを入力して読み込まれたか確認します。
詳細
ビルド時の環境変数の検証
サーバーの起動時とビルド前に環境変数を検証します。必須の変数や無効な変数を早期に検出し、明確なエラーメッセージを表示します。
環境変数の検証を実装する
サーバーの起動時とビルド前に環境変数を検証します。必須の変数や無効な変数を早期に検出し、明確なエラーメッセージを表示します。
参照:
- リソース:Fullstack Recipes の
env-validation - URL:https://fullstackrecipes.com/recipes/env-validation
サーバー起動時の設定の検証
一部の環境変数は、引数として渡されるのではなく、パッケージ内部で読み込まれます。実行時ではなく早期に必須の変数の不足を検出するには、instrumentation.ts で設定をインポートします。
// 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
}
副作用のあるインポートにより、サーバーの起動時に configSchema の検証が直ちに実行されます。必須の環境変数が不足している場合、コードパスの実行時に後から失敗するのではなく、明確なエラーとともにサーバーの起動が失敗します。
ビルド前の環境ファイルの検証
shadcn レジストリからインストールする:
bunx --bun shadcn@latest add https://fullstackrecipes.com/r/validate-env.json
またはソースコードをコピーする:
scripts/validate-env.ts:
#!/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.json に追加します。
{
"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:validate と env:validate:prod スクリプトを使用して、.env ファイルに対してすべての設定(src/lib/*/ 内の config.ts ファイル)を検証します。
(上記で設定した)prebuild スクリプトは build の前に自動的に実行され、すべてのビルド(ローカルおよび CI/Vercel)前に環境変数が検証されます。検証に失敗すると、明確なエラーとともにビルドが早期に停止します。
スクリプトは次の処理を行います。
- Next.js の
loadEnvConfigを使用して.envファイルを読み込みます(Next.js と同じ読み込み順序に従います) 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 で役立ちます)。ただし、未使用の変数はビルドを失敗させず、警告のみを表示します。