第41章:設定(Config)の境界:どこで読み、どこへ渡す?🧾
この章はね、**「環境ごとに変わる情報(Port番号、DBパス、APIキー…)が、内側(UseCase/Entity)に侵入して事故るのを防ぐ」**ための回だよ〜😊🛡️
この章のゴール🎯✨
process.envとか.envの存在を、UseCase/Entityから消せる🌪️➡️🧼- 設定を “外側で読む→型チェック→必要最小限だけ注入” できる💉✨
- テストで 設定のせいでつまづかない(=差し替えが楽)🧪🎉
まず結論:Configの鉄則3つ🧠📌
- Configは外側で読む(起動時・Composition Root)🚪
- 内側はConfigを知らない(
process.env禁止!)🙅♀️ - 渡すのは必要最小限(Config丸ごと注入しない)🍱✨
「環境差」をコードに混ぜると壊れやすいから、境界で止めるのが勝ち筋だよ😊🧱
なんで危ないの?よくある事故集😇💥
事故①:UseCaseが process.env を直読みする
- UseCaseが「外側(実行環境)」に依存しちゃう
- テストで
process.envを整えないと落ちる - 依存が“見えない”から、後で地獄👻
事故②:Configを “どこでもimportできるSingleton” にする
- いろんな層がこっそり使い始めて、いつの間にか中心が汚れる🫠
- 「何が必要な依存なのか」が隠れてレビューでも気づきにくい🙈
Configって何が入るの?仕分けが超大事🧺✨
✅ 外側のConfig(内側に入れない寄り)
- サーバーの
PORT、DB接続情報、外部サービスURL、APIキー、ログ設定など - これは「インフラ都合」だから、外側で完結させたい💡 (12-factorでも「設定は環境変数に置く」が推奨されてるよ)(12-Factor App)
✅ 内側に渡してもOKな“方針”っぽい値(ただし最小限!)
- 例:
TASK_TITLE_MAX_LENGTH(運用で変えたい上限)みたいなもの - Config丸ごとじゃなくて、数値1個だけ渡すのがコツ🍬
2026的:.envはNodeが“標準で”読めるよ📦✨
最近のNodeは .env を CLIオプションで読み込めるよ〜!
--env-file / --env-file-if-exists が公式に案内されてる👏(Node.js)
しかも .env サポートは Node 20.6.0 から入った流れ(リリースノートにもある)だよ🆕(Node.js)
--env-file:ファイルが無いとエラーになりやすい--env-file-if-exists:無くても続行してくれる(本番で便利)💡(Node.js)
※ dotenv も今も普通に使える(npmにある)から、チーム方針で選んでOKだよ😊(npmjs.com)
目標の形:データの流れ(Config版)🔁✨

外側(起動)
.env/環境変数 を読む → 型チェック → AppConfig を作る
↓
外側(組み立て)
必要な値だけ取り出して、AdapterやUseCaseへ注入💉
↓
内側(UseCase/Entity)
「ただの値」として受け取って使う(出どころは知らない)😌
実装:Configを“外側で読む→検証→注入”してみよう🛠️💕
1) .env(ローカル用)を用意🗒️
PORT=3000
DB_PATH=./data/tasks.sqlite
TASK_TITLE_MAX_LENGTH=100
NODE_ENV=development
2) Node起動で .env を読み込む(標準機能)🚀
{
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/main/index.js",
"start:dev": "node --env-file-if-exists=.env dist/main/index.js"
}
}
--env-file-if-exists は「ローカルは.env、クラウドは環境変数」みたいな切り替えに強いよ😊🧩(Node.js)
3) 外側でConfigを“型チェック”する(Zodで安心)🧪✨
// src/main/config/env.ts
import { z } from "zod";
const EnvSchema = z.object({
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
DB_PATH: z.string().min(1),
TASK_TITLE_MAX_LENGTH: z.coerce.number().int().min(1).max(200).default(100),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
});
export type AppConfig = z.infer<typeof EnvSchema>;
export function loadConfig(): AppConfig {
const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
console.error("❌ Config error:", parsed.error.format());
process.exit(1); // 起動直後に落として気づけるのが正義🛑
}
return parsed.data;
}
Zodは「スキーマ定義して検証する」ライブラリで、parse/safeParse でバリデーションできるよ🧷(Zod)
4) どこへ渡す?“必要最小限”注入の例💉🍱
// src/main/di/makeApp.ts(イメージ)
import type { AppConfig } from "../config/env";
export function makeApp(config: AppConfig) {
// ✅ DB_PATHはSQLiteRepository(外側)へ
const repo = new SqliteTaskRepository({ dbPath: config.DB_PATH });
// ✅ PORTはWebサーバ起動(外側)へ
const server = new WebServer({ port: config.PORT });
// ✅ UseCaseへ渡すなら「値だけ」🍬
const createTask = new CreateTaskInteractor({
repo,
maxTitleLength: config.TASK_TITLE_MAX_LENGTH,
});
return { server, createTask };
}
ポイントはこれ👇✨
DB_PATHを UseCaseに渡さない(それAdapterの都合!)🧠- UseCaseに渡すなら
maxTitleLengthみたいに 意味のある値1個🍭 - UseCase内に
process.envが出てきたら赤信号🚨
テストが楽になる理由🧪🎉
UseCaseがConfig直読みだと、テスト前に process.env を整える儀式が必要になるけど…
この形なら ただの引数で済むよ😊
const usecase = new CreateTaskInteractor({
repo: new FakeTaskRepository(),
maxTitleLength: 10,
});
「環境の都合」が消えると、テストが気持ちよくなる〜✨🧼
チェックリスト(設計監査用)✅👀
- UseCase/Entityに
process.env/.env/fs読み込みが無い?🚫 - Configは 起動時に一括ロードしてる?🚀
- Configは 型チェックして、ダメなら即落ちる?🛑
- UseCaseに渡すのは 最小限の値だけ?🍬
- DB接続文字列やAPIキーを 内側に渡してない?🔐
ミニ演習(手を動かす)📝✨
.envにTASK_TITLE_MAX_LENGTH=5を入れるCreateTaskでタイトル6文字を弾くようにして、ドメインエラーにする⚠️- テストでは
maxTitleLength: 5を渡して同じ挙動になるのを確認🧪🎯
AI相棒プロンプト(コピペ用)🤖✨
- 「
process.envを直接読んでる箇所があれば、クリーンアーキ的にNG理由と修正案を出して」 - 「この
EnvSchemaに足りないバリデーション観点を列挙して(ポート範囲、必須、enumなど)」 - 「ConfigをUseCaseに渡しすぎてないかレビューして。渡すべき最小単位に分解して提案して」
- 「起動時にConfigが不正なら“分かりやすいエラー表示”にしたい。例を出して」
次の章(42)で、外部サービス(通知とか)を同じノリで Port/Adapterで包んで注入して完成させるよ〜📣✨