🙌

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