ConfigModuleでNestJSの環境変数を型安全に管理する実装ガイド【@nestjs/config】
NestJSのConfigModule(公式パッケージ@nestjs/config)は、.envや環境変数を一元的に読み込み、DI経由でアプリ全体に配る設定管理の仕組みです。この記事では、forRootの基本設定から、ConfigServiceでの型安全な値取得、Joi・validate関数によるバリデーション、registerAsでの名前空間化、本番のシークレット管理、つまずきやすいエラーの対処までを、最新の4.0系(ライセンスMIT)の実装に沿って解説します。設定をどこに置き、どう検証し、どう本番へ渡すかという設計判断まで踏み込み、公式ドキュメントの先にある実務の勘所を整理します。
目次
- 1 まとめ:ConfigModule導入で押さえる設定管理の要点
- 2 ConfigModuleの役割とprocess.env直接参照との違い
- 3 @nestjs/configと旧nestjs-config・素のdotenvの使い分け
- 4 forRootの初期設定とisGlobal・envFilePathの指定パターン
- 5 ConfigServiceでの値取得と型安全化(get・getOrThrow・infer)
- 6 開発・本番・テストを分ける環境別.envの読み込み設計
- 7 環境変数バリデーションの設計:Joiとvalidate関数の選択基準
- 8 registerAsとforFeatureで設定を名前空間化する構造化手法
- 9 本番運用のcache・expandVariablesとシークレット管理の勘所
- 10 ConfigModuleのよくあるエラーと原因別の対処パターン
- 11 ConfigModuleに関するよくある質問
- 12 関連記事
まとめ:ConfigModule導入で押さえる設定管理の要点
結論として、ConfigModuleは「設定の入口を一本化し、起動時に検証し、型付きで取り出す」ための土台です。最小構成はルートのAppModuleでConfigModule.forRoot({ isGlobal: true })を呼び、各サービスでConfigServiceを注入してgetするだけで動きます。実務で差がつくのは、必須値をgetOrThrowで起動時に弾くこと、Joiまたはvalidate関数で値を検証してフェイルファストにすること、registerAsで設定を機能ごとに名前空間化することの3点です。
本番運用では、.envを本番に置かずプロセス環境変数として注入し、cache: trueで参照を高速化する構成が基本になります。以降の本文では、各設定オプションの意味と選択基準、環境別の読み込み設計、名前空間化の具体実装、そしてエラー別の対処を順に示します。まとめでは結論だけを述べ、根拠と手順は本文で扱います。
ConfigModuleの役割とprocess.env直接参照との違い
最初に、なぜ素のprocess.envではなくConfigModuleを使うのかという前提を整理します。ConfigModuleはNode.jsのバックエンド開発で頻出する「設定の散らばり」を解消する役割を担います。
設定値の散在と環境差異という運用課題の整理
規模が大きくなると、DB接続情報・APIキー・ポート番号といった設定が複数ファイルに散らばり、開発と本番で値が食い違う事故が起きます。ConfigModuleは設定の読み込み口を一箇所に集約し、こうした散在と環境差異を構造的に防ぎます。NestJSはNode.jsでのバックエンド開発を前提とするため、環境変数管理の作法はそのまま運用品質に直結します。
process.env直接参照で起きるundefinedと型欠落のリスク
process.env.PORTのような直接参照は、値が常にstring | undefined型で返り、未設定でも気づかずにundefinedのまま動いてしまいます。数値が欲しい場面でも文字列で渡るため、Number(process.env.PORT)のような変換漏れがバグの温床になります。ConfigModuleは検証と型付けの層を挟むことで、この欠落と型崩れを起動時に検知できます。
@nestjs/configが担う「一元ロード・検証・DI」の3機能
@nestjs/configの価値は、.envと環境変数の一元ロード、起動時のバリデーション、DIによる型付き配布という3機能の組み合わせにあります。内部的にはdotenvで.envを解析し、ConfigServiceというプロバイダーとして値を供給します。単なる読み込みライブラリではなく、NestJSのDIコンテナと統合されている点が本質的な違いです。
設定値がDI経由でアプリ全体へ届くまでの流れ
起動時にforRootが.envとプロセス環境変数を読み込み、ConfigServiceに格納します。各サービスやコントローラーはコンストラクタでConfigServiceを注入し、getで値を取り出します。グローバル登録すれば、このConfigServiceを再importなしでどのモジュールからでも利用できます。
ConfigModule導入が見合うプロジェクト規模の判断目安
設定値が数個でスクリプト的な用途なら、素のprocess.envでも実害は出にくいでしょう。一方、環境が3つ以上に分かれる、APIキーやDB資格情報を扱う、複数人で開発するといった条件が一つでも当てはまるなら、検証と型安全の恩恵が導入コストを上回ります。NestJSプロジェクトであれば、初期からの導入が最も安価な判断です。
@nestjs/configと旧nestjs-config・素のdotenvの使い分け
検索すると名前の似た選択肢が並び、混同が起きやすい領域です。ここで公式パッケージと旧来の手段の違いを明確にしておきます。
公式@nestjs/config(v4系・MIT)の位置づけと前提バージョン
@nestjs/configはNestJS公式が提供するパッケージで、2026年時点の最新安定版は4.0.4、ライセンスはMITです。@nestjs/commonのv10またはv11を前提とし、rxjs 7系に依存します。リポジトリはgithub.com/nestjs/config配下にあり、NestJS本体と同じ組織が保守しています。
旧nestjsx/nestjs-configとの違いと検索結果で混同しやすい点
検索上位にはnestjsx/nestjs-configという別パッケージが残っており、これは公式の@nestjs/configとは異なる古いサードパーティ製です。インストール名もインポート対象も違うため、記事やStack Overflowを参照する際は@nestjs/config(スコープ付き)かどうかを必ず確認してください。現在の新規実装では公式の@nestjs/configを選ぶのが定石です。
素のdotenv+process.envで完結できるケースの境界
NestJSを使わない小さなNode.jsスクリプトなら、dotenvを読み込んでprocess.envを参照するだけで十分なこともあります。境界線は「DIで設定を配りたいか」「起動時に検証したいか」です。これらが不要ならdotenv単体、必要ならConfigModuleという判断になります。
内部でdotenv・dotenv-expandを使う仕組みの理解
@nestjs/configは内部依存としてdotenvとdotenv-expandを持ち、.envの解析と変数展開をこれらに委ねています。つまりdotenvの知識はそのまま活きるため、.envの記法を別途学び直す必要はありません。ConfigModuleはその上にNestJS流のDIと検証を載せた層だと捉えると理解しやすいです。
3手段の比較:DI・型安全・検証・名前空間の有無
選択を整理するため、代表的な3手段を機能軸で比較します。
| 手段 | DI統合 | 起動時検証 | 型安全な取得 | 名前空間化 |
|---|---|---|---|---|
| @nestjs/config(公式) | あり | Joi/validate関数 | infer・getOrThrow | registerAs対応 |
| nestjsx/nestjs-config(旧) | あり(旧式) | 限定的 | 弱い | 限定的 |
| dotenv+process.env | なし | 自前実装 | なし | なし |
NestJSアプリで検証や型安全を求めるなら公式@nestjs/configが最も機能をカバーします。旧パッケージは既存資産の保守用途を除き、新規採用の理由は乏しいといえます。
forRootの初期設定とisGlobal・envFilePathの指定パターン
ここから実装に入ります。導入の起点はConfigModule.forRoot()のオプション設計です。
npm i @nestjs/configからAppModule登録までの手順
導入は次の手順で進めます。
npm i @nestjs/configでパッケージを追加する- プロジェクト直下に
.envを作成し、キーと値を記述する AppModuleのimportsにConfigModule.forRoot()を追加する- 値を使う側で
ConfigServiceを注入しgetで取得する
この4ステップで最小構成が動きます。引数なしのforRoot()でもデフォルトで.envが読み込まれます。
isGlobal:trueでアプリ全体の再importを省く設計
forRoot({ isGlobal: true })を指定すると、ConfigModuleがグローバルモジュールとして登録され、各機能モジュールで個別にimportする手間が不要になります。多数のモジュールから設定を参照する一般的なアプリでは、ルートで一度だけisGlobal: trueを付けるのが推奨パターンです。逆に依存関係を明示したい設計では、あえてグローバル化しない選択もあります。
envFilePathで複数.envの優先順位を制御する書き方
envFilePathには単一パスだけでなく配列も渡せます。配列で渡した場合、先に記載したファイルの値が優先されるため、['.env.local', '.env']のように「ローカル上書き → 既定値」の順で並べると環境別の上書きが表現できます。パスを省略すると.envがカレントディレクトリから読まれます。
ignoreEnvFile・skipProcessEnvで読み込み元を切り替える条件
本番のように.envファイルを使わずプロセス環境変数だけで運用する場合は、ignoreEnvFile: trueでファイル読み込みを無効化できます。さらにskipProcessEnv: trueを指定すると、ConfigService#getがprocess.envを参照しなくなり、loadで渡した設定だけを使う閉じた構成になります(既定はfalse)。テストで外部環境の影響を排除したいときに有効です。
forRootAsyncで非同期に設定を組み立てる場面
設定値を外部の秘密管理サービスやDBから取得してから組み立てたい場合は、forRootAsyncを使います。useFactory内で非同期処理を行い、その結果を設定として返せるため、起動時に動的な設定読み込みが必要なケースに適します。静的な.envだけで足りるなら、通常のforRootで十分です。
ConfigServiceでの値取得と型安全化(get・getOrThrow・infer)
ConfigModuleの実務上の中核がConfigServiceです。取得方法によって型安全の度合いが変わります。
getの戻り値がT|undefinedになる理由とデフォルト値指定
検証を設定していないConfigServiceでは、get<T>('KEY')の戻り値はT | undefinedになります。これは値が存在しない可能性をTypeScriptが正しく表現しているためです。get('PORT', 3000)のように第2引数でデフォルト値を渡すと、戻り値はundefinedを含まないTに絞り込まれます。
必須値をgetOrThrowで起動時に欠落検知する実装
DB接続文字列のように欠けてはならない値にはgetOrThrow<T>('DATABASE_URL')を使います。値が存在しなければ例外を投げ、戻り値の型もundefinedを除外した型になります。これにより「設定し忘れたまま起動して、後から落ちる」事故を起動時点で防げます。
infer:trueで設定値の型を推論させる書き方
get('database.port', { infer: true })のように第2引数でinfer: trueを渡すと、loadで定義した設定オブジェクトの構造から戻り値の型を推論させられます。手動でジェネリクスを書かずに型が付くため、ネストした設定値を取り出すときに記述が簡潔になります。
ConfigServiceで検証済みの型を固定する
ConfigServiceはConfigService<K, WasValidated>という2つの型引数を取り、第2引数をtrueにするとgetの戻り値からundefinedが外れます。バリデーションを通して全キーの存在が保証されている前提のアプリでは、ConfigService<EnvVars, true>として注入すると、毎回のnullチェックが不要になります。
コンストラクタインジェクションで使う基本形
利用側はサービスのコンストラクタでprivate readonly config: ConfigServiceを受け取り、メソッド内でthis.config.get('KEY')と呼び出します。これはNestJSの標準的なDIの作法そのままで、特別な記述は要りません。ConfigServiceはグローバル登録時はどこからでも注入できます。
開発・本番・テストを分ける環境別.envの読み込み設計
実務でつまずきやすいのが、環境ごとに異なる設定をどう切り替えるかです。設計の型をいくつか示します。
NODE_ENVに応じてenvFilePathを切り替える分岐
定番はNODE_ENVを見てenvFilePathを組み立てる方法です。たとえば`.env.${process.env.NODE_ENV}`を先頭に、共通の.envを末尾に置いた配列を渡すと、環境固有値を優先しつつ共通値を補完できます。NODE_ENV自体は起動コマンド側で設定する点に注意します。
.env.development/.env.production/.env.testの分割方針
ファイルは用途で分割し、.env.developmentはローカル開発、.env.testはテスト用のダミー値、本番は後述のとおりファイルを置かない、という方針が扱いやすいです。各ファイルに同じキーを並べ、値だけを環境ごとに変えると差分が読みやすくなります。秘密度の高い値はどのファイルにも実値を書かないのが原則です。
共通設定と環境固有設定を二段で読む優先順位
envFilePathの配列は先頭優先で評価されるため、['.env.development', '.env']と並べると、開発固有の値が共通値を上書きします。共通のデフォルトを.envに集約し、環境ごとの差分だけを各環境ファイルに書くと、重複が減りメンテナンスが楽になります。この二段構成は環境追加時の拡張も容易です。
本番では.envを置かずプロセス環境変数を使う理由
本番環境では、.envファイルをデプロイ対象に含めず、ホスティング基盤やコンテナのプロセス環境変数として値を注入するのが安全な設計です。@nestjs/configは.envが無くてもprocess.envを参照するため、ignoreEnvFile: trueを併用すれば本番ではファイルを完全に排除できます。これにより秘密情報がリポジトリやイメージに混入する経路を断てます。
テスト実行時に設定を差し替える際の注意点
テストでは.env.testを読ませるか、テスト用モジュールでloadに固定オブジェクトを渡して設定を差し替えます。前述のskipProcessEnv: trueを使うと、CI環境に紛れ込んだ環境変数の影響を遮断でき、テストの再現性が上がります。実値に依存しないダミー設定でテストを組むのが安全です。
環境変数バリデーションの設計:Joiとvalidate関数の選択基準
ConfigModuleの真価は検証にあります。検証手段は大きく2系統あり、選択基準を理解して使い分けます。
validationSchema(Joi)でスキーマ検証する基本実装
forRootのvalidationSchemaにJoiのスキーマを渡すと、起動時に環境変数が検証されます。PORTは数値、NODE_ENVは許可リストのいずれか、といった制約を宣言的に書け、不正な値があれば起動を中断します。Joiは別途インストールが必要で、宣言的に書きたいチームに向きます。
validate関数でzod・class-validatorを使う方法
validationSchemaの代わりにvalidateオプションへ関数を渡せば、Joi以外の検証ライブラリも使えます。受け取った設定オブジェクトを検証し、検証済みオブジェクトを返す関数を実装すれば、zodやclass-validatorで型と値を同時に固められます。コードファーストで型を一元管理したい場合はこちらが適します。
validatePredefinedの挙動とignoreEnvVars非推奨化
起動コマンドで先に設定済みのプロセス変数も検証対象に含めるかはvalidatePredefinedで制御し、既定はtrueです。なお従来のignoreEnvVarsは非推奨となり、validatePredefinedで表現する形に置き換えられました。古い記事のignoreEnvVarsを見かけたら、現行オプションへの読み替えが必要です。
検証失敗で起動を止めるフェイルファストの効果
検証を入れる最大の利点は、設定不備があるとアプリが起動段階で即座に落ちる「フェイルファスト」です。本番リリース直後の謎のundefined挙動を、デプロイ時点のエラーへ前倒しできます。検証メッセージにどのキーが不正かが出るため、原因特定も速くなります。
Joiとコードファーストをいつ選ぶかの判断軸
判断軸を整理します。
| 観点 | Joi(validationSchema) | validate関数(zod等) |
|---|---|---|
| 記述スタイル | 宣言的なスキーマ | コードファースト |
| 型との一元化 | 別管理になりやすい | 型と検証を同時定義 |
| 追加依存 | joi | zod / class-validator |
| 向くチーム | スキーマ志向 | TypeScript型志向 |
既にzodを多用しているプロジェクトならvalidate関数、設定検証を独立したスキーマで管理したいならJoi、という選び方が現実的です。
registerAsとforFeatureで設定を名前空間化する構造化手法
設定が増えると、フラットなキー管理は破綻します。registerAsとforFeatureで構造化する方法を扱います。
registerAsで設定を名前空間トークンにまとめる
registerAs('database', () => ({ host: process.env.DB_HOST }))のように書くと、関連する設定をdatabaseという名前空間のもとにまとめられます。返り値のファクトリはload配列に渡して登録します。設定がドメインごとに分かれるため、巨大な単一オブジェクトを避けられます。
forFeatureで機能モジュール単位に部分登録する
特定の機能モジュールだけで使う設定は、ConfigModule.forFeature(databaseConfig)でそのモジュールに部分登録できます。ルートで全設定を抱え込まず、必要なモジュールに必要な設定だけを供給する設計が可能です。機能の独立性を保ちたいモジュラー構成で有効です。
ConfigTypeで名前空間設定を型付き注入
registerAsで定義したファクトリは、ConfigType<typeof databaseConfig>で戻り値の型を取り出せます。これを使ってトークン注入すると、名前空間設定を完全な型付きで受け取れます。get('database.host')の文字列キー指定よりも、補完とリファクタリング耐性の面で優れます。
get(‘namespace.key’)とトークン注入の使い分け
名前空間設定には2つのアクセス方法があります。手軽さ重視ならconfig.get('database.host')のドット記法、型安全重視ならregisterAsのトークンを注入して設定オブジェクトごと受け取る方法です。広く浅く参照する箇所はドット記法、特定モジュールで集中的に使う箇所はトークン注入、と使い分けると無理がありません。
大規模アプリでの設定ファイル分割ディレクトリ構成
規模が大きい場合は、設定ファイルを次のように分けると見通しが良くなります。
config/database.config.ts:DB接続関連config/app.config.ts:ポートや基本設定config/auth.config.ts:認証・トークン関連
各ファイルでregisterAsを使い、forRootのloadに並べて登録します。ドメインごとにファイルが分かれるため、設定の所在が明確になります。
本番運用のcache・expandVariablesとシークレット管理の勘所
最後に、本番で安定運用するための設定とセキュリティの要点をまとめます。
cache:trueでprocess.env参照を高速化する効果と注意
forRoot({ cache: true })を指定すると、process.envの値がメモリにキャッシュされ、参照のたびに環境オブジェクトを走査するコストを抑えられます。頻繁にgetを呼ぶアプリでは効果が出ますが、起動後に環境変数を動的に書き換える運用とは相性が悪い点に注意します。通常の本番運用ではキャッシュ有効が無難です。
expandVariablesで.env内の変数展開を有効化する
.env内でURL=https://${HOST}:${PORT}のように他の変数を参照したい場合は、expandVariables: trueを指定します。これにより内部のdotenv-expandが変数を展開します。既定では無効のため、展開記法を使うときは明示的に有効化が必要です。
本番のシークレットを.envに置かず外部注入する設計
APIキーやDBパスワードなどの機密値は、本番では.envではなくコンテナや基盤の環境変数として注入します。コンテナ運用ではDockerfileでの環境変数の扱いやオーケストレーターのシークレット機能を使い、イメージに値を焼き込まないことが重要です。ConfigModule側はignoreEnvFile: trueでファイル依存を外しておきます。
.envをGit管理から外す.gitignoreと.env.exampleの運用
開発用の.envは必ず.gitignoreに追加し、リポジトリにコミットしない運用が前提です。代わりにキーの一覧だけを記した.env.exampleを共有すると、必要な設定項目をチームで把握しつつ実値の漏洩を防げます。誤コミットは履歴に残るため、発生時はキーのローテーションが必要です。
機密情報の漏洩を防ぐログ出力とアクセス制御の基本
設定値をデバッグでそのままconsole.logすると、シークレットがログ基盤に流出します。GitHubトークンなどの機密情報の管理と同様に、出力時はマスキングし、アクセス権限を最小化する運用が基本です。ConfigModuleは値を集約する分、出力箇所の管理がそのままセキュリティ品質に直結します。
ConfigModuleのよくあるエラーと原因別の対処パターン
導入時に遭遇しやすいエラーを、原因の切り分け方とともに整理します。
Cannot find module ‘@nestjs/config’の発生原因と解決
このエラーはパッケージ未インストールか、インストール名の誤りが主因です。npm i @nestjs/configでスコープ付きの正しい名前を導入したか、node_modulesが壊れていないかを確認します。旧nestjs-configを入れていた場合は名前の取り違えを疑います。
ConfigServiceがundefinedを返すときの登録漏れ確認
getが常にundefinedを返す場合、.envのキー名のタイプミスか、そもそもforRootが読めていない可能性が高いです。キー名の完全一致と、envFilePathが実在ファイルを指しているかを順に確認します。デフォルト値を一時的に渡して、値の欠落か参照ミスかを切り分けると原因が早く見えます。
グローバル未登録でConfigServiceがDIできないエラー
ConfigServiceが注入できずDIエラーになる場合、isGlobal: trueを付けていないか、対象モジュールでConfigModuleをimportしていないことが原因です。グローバル登録するか、使う側のモジュールで明示的にimportすれば解消します。ルートで一度isGlobal: trueにする方法が最も手戻りが少ないです。
型がanyになる・推論が効かない場合の見直し
getの戻り値がanyになるのは、ジェネリクスやinfer: trueを指定していないためです。loadで型付きの設定オブジェクトを返し、infer: trueまたはConfigTypeを使うと推論が効きます。検証済み前提ならConfigService<K, true>でundefinedも外せます。
.envが読まれない時のenvFilePathと実行ディレクトリ確認
.envが反映されないときは、アプリの実行ディレクトリと.envの配置場所がずれている場合が多いです。distから起動している、モノレポでルートがずれているといったケースでは、envFilePathに絶対パスや明示パスを指定して解決します。ファイルの存在と読み取り権限も併せて確認します。
ConfigModuleに関するよくある質問
導入検討時に質問の多い点を、要点に絞って回答します。
ConfigModuleはNestJSのどのバージョンで使えますか?
現行の@nestjs/config 4.0系は、@nestjs/commonのv10またはv11に対応しています。NestJS本体のメジャーバージョンに合わせてパッケージを選ぶのが基本で、古いNestJSでは対応する旧バージョンの@nestjs/configを使う必要があります。導入前に双方のバージョン整合を確認してください。
@nestjs/configを使うのにJoiは必須ですか?
必須ではありません。JoiはvalidationSchemaを使う場合のオプションであり、検証自体が不要なら何も入れずに動きます。Joi以外で検証したい場合はvalidate関数にzodやclass-validatorを使う方法もあり、検証ライブラリは自由に選べます。
forRootとforRootAsyncはどう使い分けますか?
静的な.envやプロセス環境変数だけで設定が完結するならforRootで十分です。一方、設定値を外部サービスやDBから非同期に取得してから組み立てる必要がある場合はforRootAsyncを使います。判断軸は「起動時に非同期処理を挟むかどうか」です。
.envファイルは本番環境にも置くべきですか?
推奨されません。本番ではファイルを置かず、基盤やコンテナのプロセス環境変数として値を注入する方が安全です。ignoreEnvFile: trueを併用すればファイル読み込みを無効化でき、秘密情報がイメージやリポジトリに混入する経路を断てます。
ConfigServiceの値が常に文字列になるのはなぜですか?
環境変数はprocess.env由来で、値がすべて文字列として保持されるためです。数値や真偽値として扱いたい場合は、validate関数やJoiスキーマで変換・キャストするか、設定ファクトリ内でNumber()等により明示的に型変換します。検証層で変換しておくと、利用側が型を意識せずに済みます。
関連記事
- Node.jsバックエンド開発の基礎:ConfigModuleが前提とするNode.jsバックエンドの全体像を押さえられます。
- Dockerfileの書き方と環境変数:本番でシークレットをコンテナへ注入する設計の参考になります。
- ESLintによるコード品質の標準化:プロジェクト設定の統一という観点でConfigModuleと補完関係にあります。
- Prettier(.prettierrc)の設定:設定ファイル管理の作法を整える周辺トピックです。