---
title: "@nestjs/config（ConfigModule）の使い方｜v12のZod検証・envFilePath・型安全化"
url: "https://www.issoh.co.jp/tech/details/12632/"
published: 2026-06-18
updated: 2026-09-30
categories: ["Node.js"]
publisher: "株式会社一創"
---

# @nestjs/config（ConfigModule）の使い方｜v12のZod検証・envFilePath・型安全化

`@nestjs/config` は、NestJSで `.env` と環境変数を読み込み、検証し、`ConfigService` としてDIで配る公式パッケージです。2026年8月27日の12.0.0で、バージョン番号がNest本体に揃えられ（5.xは存在しません）、環境変数の検証がJoi専用からStandard Schema対応へ変わりました。本記事は最新の12.0.1（2026年9月22日公開）を対象に、Node.js 26.5.0＋Nest 12.1.1で実際に動かした結果を添えて、設定・検証・型付け・よくあるエラーを順に整理します。

## まとめ：@nestjs/config 12で押さえる設定管理の要点

- 最新版は12.0.1。対応するのは `@nestjs/common` 11と12で、Nest 10のままなら4.0.4に留めます。
- `ConfigModule.forRoot({ isGlobal: true })` をAppModuleに1回書けば、各モジュールでの再importは不要です。
- `envFilePath` に複数のファイルを並べると**先頭のファイルが優先**され、シェルの環境変数は `.env` より優先されます（逆にしたいときは12で追加された `override: true`）。
- `.env` や環境変数から読んだ値は、検証をしないと `ConfigService.get()` でも文字列のまま返ります。`z.coerce.number()` などで検証すると、変換後の値が返ります。
- 検証スキーマはZod・Valibot・ArkType・Joi 18以上のどれでも渡せます。新規ならZodなどのStandard Schemaライブラリが公式の推奨です。
- `ConfigModule` に `forRootAsync` はありません。非同期で組み立てるのはTypeORMなど**設定を使う側**のモジュールです。

以下、4系からの変更点、読み込み順、値の取り出し方、検証、名前空間化、テスト、エラー対処の順に説明します。

## @nestjs/config 12.0の変更点と対応バージョン

### 4.0.4から12.0.0への版番号の飛び方と対応Nest

npmレジストリ上の版の並びは、4.0.4（2026年4月9日）の次が12.0.0（2026年8月27日）です。公式リリースノートは「5.xは無く、Nest 12の系列に版を揃えた」と明記しています。対応関係は次のとおりです。

| 項目                  | 4.0.4    | 12.0.1                |
| ------------------- | -------- | --------------------- |
| 対応する @nestjs/common | 10・11    | 11・12                 |
| モジュール形式             | CommonJS | ESM（”type”: “module”） |
| 検証スキーマ              | Joi前提    | Standard Schema       |
| 内部の依存               | lodash   | es-toolkit            |
| dotenv              | 17系      | 18.0.3                |
| override・parser     | なし       | あり                    |

ESM化されましたが、CommonJSのアプリは書き換えずに使えます。Node.jsの `require(esm)`（Node 20.19以上／22.12以上）で読み込めるためで、Node 26.5.0で `require('@nestjs/config')` が通ることを確認しました。

### 4系から上げるときに書き換える箇所

アプリ側の主な修正は次の3点です。第一に、`@nestjs/config/dist/config.service` のような内部パスへのimportは `ERR_PACKAGE_PATH_NOT_EXPORTED` で失敗するので、パッケージ直下からimportし直します。第二に、Joiを使っている場合はJoiを18以上に上げ、`validationOptions: { allowUnknown: false }` を `validationOptions: { libraryOptions: { allowUnknown: false } }` へ移します。第三に、検証エラーの文言が「`キー: メッセージ`」の改行区切りに変わったため、エラー文字列を比較しているテストは期待値を直します。

## インストールとforRootの基本設定

### npm installからAppModule登録までのコード

```
npm i @nestjs/config
npm i zod        # 環境変数を検証する場合のみ
```

```
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
```

引数なしの `forRoot()` は、プロセスを起動したディレクトリの `.env` を読みます。`forRoot()` 自体が `Promise` を返す非同期関数ですが、`imports` にそのまま置いてかまいません。

### isGlobalを付けない場合に出るDIエラー

`isGlobal` を省き、`ConfigModule` をimportしていない機能モジュールで `ConfigService` を注入すると、起動時に次のエラーになります。

```
Nest can't resolve dependencies of the NeedsConfig (?). Please make sure that
the argument ConfigService at index [0] is available in the FeatureNoImport module.
```

（NeedsConfig と FeatureNoImport は検証用に作ったクラス名です）

ルートで `isGlobal: true` にするか、そのモジュールの `imports` に `ConfigModule` を足せば解消します。機能モジュールごとにimportを書く方式は追加のたびに漏れが起きるので、最初から `isGlobal: true` にしておく方が確実です。

### main.tsでポート番号を読む書き方

```
// main.ts
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = app.get(ConfigService);
  await app.listen(config.get('PORT', 3000));
}
bootstrap();
```

`main.ts` はDIの外なので、`app.get(ConfigService)` でインスタンスを取り出します。

## .envの読み込み順と環境別ファイルの分け方

### envFilePathの配列は先頭が優先

公式ドキュメントのとおり、同じ変数が複数のファイルにあれば**配列の先頭のファイル**が勝ちます。`envFilePath: ['.env.local', '.env']` で両方に `SHARED` を書くと、`get('SHARED')` は `.env.local` の値でした。個人用の上書きファイルを前に、共通ファイルを後ろに並べます。

### シェルの環境変数と.envの優先順位（overrideオプション）

既定では、起動前から `process.env` にある値が `.env` より優先されます。`FROM_ENV=shell` を付けて起動すると、`.env` に `FROM_ENV=dotenv` があっても `shell` が返りました。12.0.0で追加された `override: true` を指定すると `dotenv` が返ります。本番でコンテナ基盤から渡す値を `.env` が上書きしてしまうので、`override` はローカル開発に限って使います。

### NODE\_ENVによる読み込みファイルの切り替え

```
const env = process.env.NODE_ENV ?? 'development';

ConfigModule.forRoot({
  isGlobal: true,
  envFilePath: [`.env.${env}.local`, `.env.${env}`, '.env'],
});
```

`NODE_ENV=test` で起動すると `.env.test` の値が、未指定なら `.env.development` の値が返ることを確認しました。存在しないファイルは読み飛ばされるので、`.env.test.local` が無くてもエラーにはなりません。`*.local` と実値を入れた `.env` は `.gitignore` に入れ、キー名だけを書いた `.env.example` をコミットします。

### 本番で.envを置かない構成（ignoreEnvFile）

本番のシークレットはファイルに書かず、Kubernetes Secret や AWS Secrets Manager などからプロセスの環境変数として渡し、リポジトリやイメージに値を残さない構成にします。その場合は `ignoreEnvFile: true` で `.env` の読み込みを止め、誤って同梱された `.env` が読まれる事故を防ぎます。

### expandVariablesによる${VAR}の展開

`.env` に `DB_URL=postgres://${DB_HOST}:5432/app` と書いても、既定では `${DB_HOST}` が文字どおり残ります。`expandVariables: true` を付けると `postgres://db.local:5432/app` に展開されました。内部では dotenv-expand 13 が使われ、オブジェクトを渡すとそのオプションになります。

## ConfigService.get()の探索順と戻り値の型

### get()が値を探す4段階の順序

12.0.1のソースでは、`get()` は次の順に値を探し、最初に見つかったものを返します。

| 順 | 探す場所        | 入る経路                      |
| - | ----------- | ------------------------- |
| 1 | カスタム設定      | load・registerAs           |
| 2 | 検証済みの環境変数   | validationSchema・validate |
| 3 | process.env | .env・シェル                  |
| 4 | 既定値         | get()の第2引数                |

この順序から2つの挙動が出てきます。まず、`skipProcessEnv: true` にすると3段目を飛ばすため、検証も `load` もしていない構成では `.env` に書いた `PORT` まで `undefined` になりました。次に、検証済みの値は起動時の値に固定されるので、起動後に `process.env` を書き換えても `get()` の結果は変わりません。`cache: true` を付けると、3段目の値は最初に `get()` で読んだ時点の値に固定されます。

### 検証なしでは文字列、検証ありでは変換後の値

環境変数は常に文字列です。検証をしない構成で `.env` に `PORT=4000` と書くと、`get('PORT')` は文字列の `"4000"` を返しました。`get<number>('PORT')` と書いても、型引数はコンパイラへのヒントに過ぎず値は変換されません。`z.coerce.number()` で検証すると数値の `4000` が返り、Joi 18の `Joi.number()` でも同じく数値になりました。ただし `process.env.PORT` 自体は文字列の `"4000"` のままです。

### getOrThrowによる必須値の起動時チェック

存在しないキーを `get()` で読むと `undefined` が返り、第2引数を渡せばその既定値が返ります。`getOrThrow('MISSING')` は `TypeError: Configuration key "MISSING" does not exist` を投げます。APIキーのように欠けると動かない値は、コンストラクタで `getOrThrow` して起動時に落とす方が、リクエストの途中で失敗するより原因を追いやすくなります。

## ConfigService<Env, true>とinferによる型安全化

```
interface Env {
  PORT: number;
  DB: { host: string };
}

@Injectable()
export class AppService {
  constructor(private readonly config: ConfigService<Env, true>) {}

  connection() {
    const port = this.config.get('PORT', { infer: true });     // number
    const host = this.config.get('DB.host', { infer: true });  // string
    return { host, port };
  }
}
```

TypeScript 7.0.2で型を確かめた結果です。型引数を付けない `ConfigService` の `get('PORT')` は `any` で、何にでも代入できてしまいます。`ConfigService<Env>` に `infer: true` を付けると `number | undefined`、第2型引数を `true` にすると `undefined` が外れて `number` になり、`Env` に無いキーを渡すとコンパイルエラーになりました。

第2型引数の `true` は「検証済みなので値は必ずある」という宣言で、実行時には何も確かめません。`ConfigService<Env, true>` を使うのは、次の章のスキーマで全キーを必須にしている場合に限ります。`Env` はZodなら `z.infer<typeof schema>` で生成でき、スキーマと型の二重管理を避けられます。

## 環境変数の検証：Zod・Joi 18・validate関数

### validationSchemaへのZodスキーマ指定

```
import { z } from 'zod';

export const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(3000),
  API_KEY: z.string(),
});
export type Env = z.infer<typeof envSchema>;

ConfigModule.forRoot({ isGlobal: true, validationSchema: envSchema });
```

`API_KEY` を欠き、`NODE_ENV` に想定外の値を入れて起動すると、2件がまとめて報告されました。

```
Config validation error: NODE_ENV: Invalid option: expected one of "development"|"production"|"test"
API_KEY: Invalid input: expected string, received undefined
```

Zodの `z.object()` は宣言していないキーを捨てますが、12.0.0からは捨てられた変数も結果に戻されるため、スキーマに書いていない変数も検証済みの値に含まれ、`skipProcessEnv: true` でも `get()` で読めます（`PORT` だけを宣言したスキーマで `DB_HOST` と `PATH` が返りました）。

### Joiを使い続ける場合の条件

Joiは18以上ならそのまま `validationSchema` に渡せます。17以前はStandard Schemaを実装していないため型エラーになります。`allowUnknown: true` と `abortEarly: false` の既定値は12でもJoiに限って自動で入り、Joi 18.2.9で `API_KEY` を欠いたときは `Config validation error: API_KEY: "API_KEY" is required` になりました。既にJoiで書いたスキーマがあるなら移行を急ぐ必要はありませんが、新規に書くならZodを選びます。公式ドキュメントも新規プロジェクトにはZodなどを推奨しています。

### validate関数を使う場面

`validate` は環境変数のオブジェクトを受け取り、検証後のオブジェクトを返す関数です。class-validatorのDTOで検証したい場合や、「`NODE_ENV` が production のときだけ `SENTRY_DSN` を必須にする」のような条件付きの規則を書きたい場合に使います。`validate` と `validationSchema` を両方渡すと、`validate` だけが実行されます。

### 検証エラーが表示されずに落ちるケース

検証に失敗すると、例外で起動が止まります。ただし `NestFactory.create` に `logger: false` を渡していると、メッセージを出さずに終了コード1で終わりました。CIでコンテナが原因不明のまま再起動を繰り返すときは、ロガーを止めていないかを最初に確認します。

## registerAsによる設定の名前空間化

### registerAsとConfigTypeによる型付き設定の注入

```
// config/database.config.ts
export const databaseConfig = registerAs('database', () => ({
  host: process.env.DB_HOST ?? 'localhost',
  port: parseInt(process.env.DB_PORT ?? '5432', 10),
}));

// 利用側
constructor(
  @Inject(databaseConfig.KEY)
  private readonly db: ConfigType<typeof databaseConfig>,
) {}
```

`ConfigModule.forRoot({ load: [databaseConfig] })` か、機能モジュール側の `ConfigModule.forFeature(databaseConfig)` で登録します。`this.db.host` は `string` 型で補完が効き、同じ値は `get('database.host')` でも読めます。新しく書くコードではトークン注入を使い、文字列キーの打ち間違いをコンパイル時に検出します。

### TypeORMなどのforRootAsyncへの設定受け渡し

「NestJSの設定を非同期で読む」ときに使う `forRootAsync` は、`ConfigModule` ではなく、TypeORMのように**設定を受け取る側**のモジュールが持つメソッドです。名前空間の設定には `asProvider()` があり、戻り値は `imports`・`useFactory`・`inject` の3つを持つオブジェクトなので、そのまま渡せます。

```
TypeOrmModule.forRootAsync(databaseConfig.asProvider())
```

TypeORMに渡す場合は、`registerAs` の戻り値に `type: 'postgres'` や `username` など、TypeORMの接続オプション一式を含めます。上の `host` と `port` だけでは接続できません。なお、`forFeature` で登録した値は、別モジュールのコンストラクタから読むと初期化順によってまだ無いことがあります。公式ドキュメントは `onModuleInit()` で読むよう注意しています。

## テストで設定を差し替える方法

```
const moduleRef = await Test.createTestingModule({
  imports: [
    ConfigModule.forRoot({
      ignoreEnvFile: true,
      load: [() => ({ API_URL: 'http://mock' })],
    }),
  ],
  providers: [ApiService],
}).compile();
```

`load` の値は探索順の1段目なので、開発者の `.env` やCIの環境変数に何が入っていても、テストでは `http://mock` が返りました。`ConfigService` ごと差し替えるなら `.overrideProvider(ConfigService).useValue({ get: () => 'http://stub' })` も動きます。前者は `getOrThrow` や `infer` の挙動がそのまま残るので、通常は前者を使います。

## ConfigModuleのよくあるエラーと対処

| 症状                                           | 主な原因                   | 対処                     |
| -------------------------------------------- | ---------------------- | ---------------------- |
| `Cannot find module '@nestjs/config'`        | 未インストール                | `npm i @nestjs/config` |
| `ERR_PACKAGE_PATH_NOT_EXPORTED`              | dist配下の直接import        | パッケージ直下からimport        |
| `can't resolve dependencies … ConfigService` | ConfigModule未import    | `isGlobal: true`       |
| .envの値がundefined                             | 起動ディレクトリ違い             | envFilePathを絶対パスに      |
| .envの値がundefined                             | `skipProcessEnv: true` | 検証かloadを併用             |
| 数値のはずが文字列                                    | 検証なし                   | `z.coerce.number()`    |
| npm installでERESOLVE                         | Nest 10との組合せ           | `@nestjs/config@4`を指定  |

モノレポで `apps/api` 以外のディレクトリから起動すると、既定の `.env` はそのディレクトリを探すため見つかりません。`envFilePath` を `join(__dirname, '..', '.env')`（ESMのプロジェクトでは `import.meta.dirname`）のように組むか、起動ディレクトリを固定します。

## ConfigModuleを入れずに済む構成

環境変数が数個で、DIで設定を差し替える必要もない小さなアプリなら、ConfigModuleは入れなくてかまいません。Node.jsの `--env-file=.env` はv24.10.0とv22.21.0で実験的機能の扱いが外れており、`node --env-file=.env dist/main.js` で `process.env` に読み込めます。起動時の検証だけが欲しいなら、`main.ts` の先頭で `envSchema.parse(process.env)` を1行呼べば足ります。

逆に、機能モジュールが増えてテストで設定を差し替えたくなった時点、または `registerAs` で設定をまとめたくなった時点が、ConfigModuleへ移る目安です。dotenvそのものの仕組みと言語ごとの使い方は[dotenvで.envを管理する仕組みの解説](/tech/details/3573/)で整理しています。

## ConfigModuleに関するよくある質問

### @nestjs/configの最新バージョンはいくつですか？

2026年9月30日時点のnpmのlatestは12.0.1（9月22日公開）です。`@nestjs/common` 11と12に対応し、Nest 10では4.0.4を使います。

### ConfigModuleにforRootAsyncはありますか？

ありません。`forRoot()` 自体が非同期関数で、`load` にPromiseを返す関数も渡せます。`forRootAsync` はTypeORMなど設定を受け取る側のモジュールのメソッドです。

### @nestjs/configを使うのにJoiは必須ですか？

必須ではありません。12からはZod・Valibot・ArkTypeなどStandard Schemaを実装したライブラリを選べ、Joiを使う場合は18以上が必要です。

### ConfigServiceの値が文字列になるのはなぜですか？

環境変数はOSの仕様で常に文字列だからです。`get<number>()` は型の注釈で、変換はしません。数値で受け取るには `z.coerce.number()` などで検証します。

### .envファイルは本番環境にも置くべきですか？

置かない構成を勧めます。本番の値は実行基盤からプロセスの環境変数で渡し、`ignoreEnvFile: true` で `.env` を読まないようにします。

## 関連記事

- [dotenvとは？.envで環境変数を管理する仕組みと言語別の使い方](/tech/details/3573/)
- [DTOとは？意味・Entityとの違いとNestJSのValidationPipe設定【v12実測】](/tech/details/6208/)
- [TypeScriptバリデーション断片化を解消するStandard Schemaの全体像](/tech/details/11777/)
- [Zodの使い方入門｜npmでのインストールとz.infer・safeParse・Zod 4.6の変更点](/tech/details/7391/)
- [TypeORMとは？1.1系の使い方｜エンティティ・リレーション・マイグレーションを実行して解説](/tech/details/7599/)

---

出典: [@nestjs/config（ConfigModule）の使い方｜v12のZod検証・envFilePath・型安全化](<https://www.issoh.co.jp/tech/details/12632/>)（株式会社一創）
