dotenvx で「環境変数管理地獄」を脱出する方法
最近、環境変数まわりの事故を減らすために、
dotenvx + envalid + Biome の組み合わせで 「環境変数管理地獄」からの脱出を進めました。
結論からいうと、
-
.env*のロードは dotenvx - ランタイム型安全(バリデーション)は envalid
-
process.env直読み禁止は Biome(lint)
この3点を揃えるだけで、環境変数まわりのストレスはかなり減ります。
特に monorepo 構成では package ごとに env スキーマを分ける運用が現実的になります。
この記事では TypeScript プロジェクトを例に書きますが、
dotenvx は言語非依存(Node / Bun / Go / Rust / Python など)なので、
ロード周りの考え方はそのまま他言語にも応用できます。
1. なぜ process.env.FOO || "xxx" は地獄なのか
環境変数まわりの “よくある書き方” がこちらです。
const baseUrl = process.env.BASE_URL || "http://localhost:3000";
一見問題なさそうですが、この書き方は将来必ず自分を裏切ります。
falsy 問題
process.env.TIMEOUT = "0";
この場合、"0" は falsy として評価され、
意図せず fallback が使われます。
値は入っているのに無視される
という非常に気づきにくいバグになります。
隠れた本番バグを生みやすい
本番 .env.prod に値を入れ忘れても、
fallback によってアプリは動いてしまう。
結果として、
- 設定ミスに気づかない
- 本番だけ微妙に挙動が違う
といった事故が起きがちです。
型が永遠に string | undefined
TypeScript 的にも厳しく、
process.env.FOO // string | undefined
という型が永遠につきまといます。
本来不要な if (!FOO) ガードを量産することになります。
2. 要件整理
今回やりたかったことを整理すると、以下です。
- 必須 env がなければ 起動時に落ちてほしい
- ステージごとに
.envを分けたい(dev / stg / prod) - monorepo / package ごとに env スキーマを分けたい
- TypeScript で「env は必ずある」前提で書きたい
- Linter で
process.env直読みを禁止したい
最近は フロントエンドもサーバーサイドも TypeScript で書くことが増えてきたため、
型を起点に env を管理できることが重要になってきています。
3. 採用した組み合わせ
dotenvx
.env* のロード担当です。
最大の特徴は 言語非依存 な点で、Node 以外でも同じ思想で使えます。
envalid
- 必須 env のチェック
- number / boolean への変換
- ランタイム型安全
を担います。
起動時に落ちるという性質が非常に重要です。
Biome
process.env の直読みを lint で禁止します。
「env.ts 以外では env を読めない」文化を強制できます。
4. 設計方針
.env* は「値の差分」だけを書く
-
.env: 全環境共通 -
.env.stg: ステージ固有 -
.env.prod: 本番固有
スキーマ(型)は package ごとに 1 つ
packages/be/env.ts
packages/be-idp/env.ts
packages/fe-admin/env.ts
process.env を触っていいのは env.ts だけ
それ以外のコードでは、必ず env オブジェクト経由で参照します。
5. 実装ステップ
STEP 1. envalid を導入
npm install envalid
STEP 2. env.ts を作る
import { cleanEnv, str, num, bool } from "envalid";
export const env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ["development", "staging", "production"] }),
PORT: num(),
DATABASE_URL: str(),
ENABLE_METRICS: bool({ default: false }),
});
STEP 3. Biome で直読みを禁止
{
"linter": {
"rules": {
"security": {
"noProcessEnv": "error"
}
}
},
"files": {
"ignore": ["**/env.ts"]
}
}
STEP 4. dotenvx で起動
dotenvx run -- node dist/index.js
STEP 5. 既存コードの置き換え
import { env } from "./env";
const url = env.API_URL;
6. 効果とメリット
- 起動時に env 不備を即検出できる
-
env.Xがすべて正しい型になる - package ごとに env の責務が明確になる
- 新規メンバーが
.env周りで迷いにくい
まとめ
dotenvx × envalid × Biome は、
TypeScript × monorepo 環境で非常に相性が良い組み合わせです。
環境変数に少しでもストレスを感じているなら、
一度この構成を試してみる価値はあります。
Discussion