アプリケーションは開発、テスト、本番など複数の環境で動作します。各環境でデータベース接続情報やAPIキーなどの設定値が異なるため、これらを適切に管理することは堅牢なアプリケーション構築の基盤となります。

NestJSでは@nestjs/configパッケージを使用することで、環境変数の読み込み、型安全な設定値の取得、バリデーションを統一的に実装できます。本記事では、ConfigModuleConfigServiceを活用した環境設定の一元管理方法を解説します。

前提条件

本記事の内容を実践するには、以下の環境が必要です。

項目 バージョン・条件
Node.js 18.x 以上
NestJS 10.x 以上
TypeScript 4.1 以上
パッケージマネージャ npm または yarn

NestJSプロジェクトが作成済みであることを前提とします。未作成の場合は以下のコマンドで作成してください。

1
2
npm i -g @nestjs/cli
nest new my-nestjs-app

@nestjs/configパッケージのインストール

まず、@nestjs/configパッケージをインストールします。

1
npm install --save @nestjs/config

このパッケージは内部的にdotenvを使用しており、.envファイルから環境変数を読み込む機能を提供します。

ConfigModuleの基本設定

forRoot()によるグローバル設定

ConfigModuleをアプリケーションのルートモジュールにインポートし、forRoot()メソッドで初期化します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

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

この設定により、プロジェクトルートディレクトリにある.envファイルが自動的に読み込まれます。

.envファイルの作成

プロジェクトルートに.envファイルを作成し、環境変数を定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# .env
NODE_ENV=development
PORT=3000

# データベース設定
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=secret
DATABASE_NAME=myapp_dev

# JWT設定
JWT_SECRET=your-super-secret-key
JWT_EXPIRES_IN=1h

.envファイルは.gitignoreに追加し、バージョン管理から除外してください。代わりに.env.exampleファイルを作成して、必要な環境変数のテンプレートを共有します。

isGlobalオプションでグローバルモジュール化

デフォルトでは、ConfigModuleを使用する各モジュールで個別にインポートする必要があります。isGlobalオプションをtrueに設定すると、一度インポートするだけでアプリケーション全体で使用可能になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

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

この設定により、他のモジュールでConfigModuleをインポートせずにConfigServiceを注入できます。

ConfigServiceによる設定値の取得

基本的な設定値の取得

ConfigServiceを依存性注入してコンストラクタで受け取り、get()メソッドで設定値を取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
  constructor(private readonly configService: ConfigService) {}

  getDatabaseHost(): string {
    // 環境変数DATABASE_HOSTを取得
    return this.configService.get<string>('DATABASE_HOST');
  }

  getPort(): number {
    // 型指定で取得(デフォルト値付き)
    return this.configService.get<number>('PORT', 3000);
  }
}

get()メソッドの第一引数にキー名、第二引数にデフォルト値を指定できます。デフォルト値を指定すると、環境変数が未定義の場合にその値が返されます。

型安全な設定値の取得

TypeScriptのジェネリクスを活用することで、ConfigServiceから型安全に設定値を取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/config/environment-variables.interface.ts
export interface EnvironmentVariables {
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: number;
  DATABASE_HOST: string;
  DATABASE_PORT: number;
  DATABASE_USERNAME: string;
  DATABASE_PASSWORD: string;
  DATABASE_NAME: string;
  JWT_SECRET: string;
  JWT_EXPIRES_IN: string;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvironmentVariables } from './config/environment-variables.interface';

@Injectable()
export class AppService {
  constructor(
    private readonly configService: ConfigService<EnvironmentVariables>,
  ) {}

  getConfig() {
    // inferオプションで型推論を有効化
    const port = this.configService.get('PORT', { infer: true });
    // portの型はnumber | undefined

    // 存在しないキーを指定するとTypeScriptエラー
    // const invalid = this.configService.get('INVALID_KEY', { infer: true });
    // TypeScript Error: Argument of type '"INVALID_KEY"' is not assignable

    return { port };
  }
}

infer: trueオプションを使用すると、インターフェースに基づいた型推論が有効になります。

undefined を除外する設定

strictNullChecksが有効な環境でundefinedを除外したい場合、ConfigServiceの第二ジェネリクス引数にtrueを指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Injectable()
export class AppService {
  constructor(
    private readonly configService: ConfigService<EnvironmentVariables, true>,
  ) {}

  getPort(): number {
    // 戻り値の型はnumber(undefinedなし)
    return this.configService.get('PORT', { infer: true });
  }
}

この設定により、必須の環境変数が未定義の場合にランタイムエラーではなく型エラーとして検出できます。

カスタム設定ファイルによる構造化

複雑なアプリケーションでは、環境変数を機能ごとにグループ化し、ネストされた設定オブジェクトとして管理すると可読性が向上します。

設定ファクトリ関数の作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// src/config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    username: process.env.DATABASE_USERNAME,
    password: process.env.DATABASE_PASSWORD,
    name: process.env.DATABASE_NAME,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '1h',
  },
});

loadオプションで設定ファイルを読み込む

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';

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

ネストされた設定値の取得

ドット記法を使用して、ネストされた設定値にアクセスします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/database/database.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class DatabaseService {
  constructor(private readonly configService: ConfigService) {}

  getConnectionConfig() {
    return {
      host: this.configService.get<string>('database.host'),
      port: this.configService.get<number>('database.port'),
      username: this.configService.get<string>('database.username'),
      password: this.configService.get<string>('database.password'),
      database: this.configService.get<string>('database.name'),
    };
  }
}

名前空間による設定の分離

registerAs()関数を使用すると、設定を名前空間で分離し、より明確な構造で管理できます。

名前空間付き設定の定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/config/database.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST || 'localhost',
  port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  name: process.env.DATABASE_NAME,
}));
1
2
3
4
5
6
7
// src/config/jwt.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('jwt', () => ({
  secret: process.env.JWT_SECRET,
  expiresIn: process.env.JWT_EXPIRES_IN || '1h',
}));

複数の設定ファイルを読み込む

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';

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

名前空間の直接注入

@Inject()デコレータとConfigTypeを使用することで、名前空間を直接注入し、強い型付けを実現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/auth/auth.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import jwtConfig from '../config/jwt.config';

@Injectable()
export class AuthService {
  constructor(
    @Inject(jwtConfig.KEY)
    private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
  ) {}

  getTokenConfig() {
    // 強い型付けによるアクセス
    return {
      secret: this.jwtConfiguration.secret,
      expiresIn: this.jwtConfiguration.expiresIn,
    };
  }
}

環境ごとの設定分離

開発、テスト、本番など環境ごとに異なる.envファイルを使用する方法を解説します。

envFilePathオプションの使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
    }),
  ],
})
export class AppModule {}

この設定により、NODE_ENVの値に応じて.env.development.env.production.env.testなどのファイルが読み込まれます。

複数の.envファイルを指定

配列で複数のファイルを指定すると、最初に見つかったファイルの値が優先されます。

1
2
3
4
5
6
7
8
9
ConfigModule.forRoot({
  isGlobal: true,
  envFilePath: [
    `.env.${process.env.NODE_ENV}.local`,  // 最優先
    `.env.${process.env.NODE_ENV}`,
    '.env.local',
    '.env',                                 // フォールバック
  ],
}),

環境変数ファイルの構成例

1
2
3
4
5
6
7
project-root/
├── .env                    # 共通のデフォルト値
├── .env.development        # 開発環境用
├── .env.production         # 本番環境用
├── .env.test               # テスト環境用
├── .env.example            # テンプレート(Git管理対象)
└── .gitignore              # .env* を除外(.env.exampleは除く)

.gitignoreの設定

1
2
3
# .gitignore
.env*
!.env.example

環境変数のバリデーション

本番環境でのエラーを防ぐため、アプリケーション起動時に環境変数を検証することが重要です。

Joiによるスキーマバリデーション

Joiパッケージを使用したバリデーションを設定します。

1
npm install --save joi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test')
          .default('development'),
        PORT: Joi.number().port().default(3000),
        DATABASE_HOST: Joi.string().required(),
        DATABASE_PORT: Joi.number().port().default(5432),
        DATABASE_USERNAME: Joi.string().required(),
        DATABASE_PASSWORD: Joi.string().required(),
        DATABASE_NAME: Joi.string().required(),
        JWT_SECRET: Joi.string().min(32).required(),
        JWT_EXPIRES_IN: Joi.string().default('1h'),
      }),
      validationOptions: {
        allowUnknown: true,   // 未定義のキーを許可
        abortEarly: false,    // すべてのエラーを収集
      },
    }),
  ],
})
export class AppModule {}

必須の環境変数が不足している場合、アプリケーション起動時に明確なエラーメッセージが表示されます。

class-validatorによるカスタムバリデーション

class-validatorclass-transformerを使用した、より柔軟なバリデーション方法も利用できます。

1
npm install --save class-validator class-transformer
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// src/config/env.validation.ts
import { plainToInstance } from 'class-transformer';
import {
  IsEnum,
  IsNumber,
  IsString,
  Max,
  Min,
  MinLength,
  validateSync,
} from 'class-validator';

enum Environment {
  Development = 'development',
  Production = 'production',
  Test = 'test',
}

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  @Min(1)
  @Max(65535)
  PORT: number;

  @IsString()
  DATABASE_HOST: string;

  @IsNumber()
  @Min(1)
  @Max(65535)
  DATABASE_PORT: number;

  @IsString()
  DATABASE_USERNAME: string;

  @IsString()
  DATABASE_PASSWORD: string;

  @IsString()
  DATABASE_NAME: string;

  @IsString()
  @MinLength(32)
  JWT_SECRET: string;

  @IsString()
  JWT_EXPIRES_IN: string;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToInstance(EnvironmentVariables, config, {
    enableImplicitConversion: true,
  });

  const errors = validateSync(validatedConfig, {
    skipMissingProperties: false,
  });

  if (errors.length > 0) {
    const errorMessages = errors
      .map((error) => Object.values(error.constraints).join(', '))
      .join('\n');
    throw new Error(`環境変数のバリデーションに失敗しました:\n${errorMessages}`);
  }

  return validatedConfig;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { validate } from './config/env.validation';

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

main.tsでの設定値使用

main.tsでアプリケーション起動時に設定値を使用する方法を解説します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/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);

  // ConfigServiceを取得
  const configService = app.get(ConfigService);

  // 設定値を使用
  const port = configService.get<number>('PORT', 3000);
  const nodeEnv = configService.get<string>('NODE_ENV');

  await app.listen(port);

  console.log(`Application is running on port ${port} in ${nodeEnv} mode`);
}
bootstrap();

他モジュールとの連携

@nestjs/configは他のNestJSモジュールと連携して、動的な設定を実現できます。

TypeORMとの連携

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get<string>('DATABASE_HOST'),
        port: configService.get<number>('DATABASE_PORT'),
        username: configService.get<string>('DATABASE_USERNAME'),
        password: configService.get<string>('DATABASE_PASSWORD'),
        database: configService.get<string>('DATABASE_NAME'),
        autoLoadEntities: true,
        synchronize: configService.get<string>('NODE_ENV') !== 'production',
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

asProvider()による簡潔な連携

registerAs()で作成した名前空間付き設定は、asProvider()メソッドで他モジュールに直接渡せます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/config/database.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  type: 'postgres' as const,
  host: process.env.DATABASE_HOST || 'localhost',
  port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  database: process.env.DATABASE_NAME,
  autoLoadEntities: true,
  synchronize: process.env.NODE_ENV !== 'production',
}));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import databaseConfig from './config/database.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [databaseConfig],
    }),
    TypeOrmModule.forRootAsync(databaseConfig.asProvider()),
  ],
})
export class AppModule {}

パフォーマンス最適化

キャッシュの有効化

process.envへのアクセスはパフォーマンスに影響を与える可能性があります。cacheオプションを有効にすると、設定値がキャッシュされ、パフォーマンスが向上します。

1
2
3
4
ConfigModule.forRoot({
  isGlobal: true,
  cache: true,
}),

変数の展開

.envファイル内で他の変数を参照する場合、expandVariablesオプションを有効にします。

1
2
3
4
# .env
APP_URL=myapp.example.com
API_URL=https://${APP_URL}/api
SUPPORT_EMAIL=support@${APP_URL}
1
2
3
4
ConfigModule.forRoot({
  isGlobal: true,
  expandVariables: true,
}),

設定管理のアーキテクチャ図

以下は@nestjs/configを使用した設定管理の全体像を示す図です。

flowchart TB
    subgraph "環境変数ソース"
        ENV_FILE[".envファイル"]
        SYSTEM_ENV["システム環境変数"]
        CONFIG_FILES["カスタム設定ファイル"]
    end

    subgraph "ConfigModule"
        FORROOT["ConfigModule.forRoot()"]
        VALIDATION["バリデーション<br/>Joi / class-validator"]
        MERGE["設定値のマージ"]
    end

    subgraph "設定の提供"
        CONFIG_SERVICE["ConfigService"]
        NAMESPACE["名前空間<br/>registerAs()"]
    end

    subgraph "利用側"
        SERVICE["Service"]
        CONTROLLER["Controller"]
        OTHER_MODULE["他モジュール<br/>TypeORM等"]
        MAIN["main.ts"]
    end

    ENV_FILE --> FORROOT
    SYSTEM_ENV --> FORROOT
    CONFIG_FILES --> FORROOT
    FORROOT --> VALIDATION
    VALIDATION --> MERGE
    MERGE --> CONFIG_SERVICE
    MERGE --> NAMESPACE

    CONFIG_SERVICE --> SERVICE
    CONFIG_SERVICE --> CONTROLLER
    CONFIG_SERVICE --> MAIN
    NAMESPACE --> OTHER_MODULE

まとめ

@nestjs/configパッケージを使用することで、NestJSアプリケーションの環境設定を型安全かつ柔軟に管理できます。本記事で解説した主要なポイントを整理します。

機能 説明
ConfigModule.forRoot() ルートモジュールでの初期化と.envファイルの読み込み
isGlobal: true グローバルモジュール化で各モジュールでのインポート不要
ConfigService 型安全な設定値の取得
registerAs() 名前空間による設定の分離と構造化
envFilePath 環境ごとの設定ファイル切り替え
validationSchema / validate Joiまたはclass-validatorによるバリデーション
cache: true パフォーマンス最適化のためのキャッシュ

環境変数の適切な管理は、アプリケーションのセキュリティと保守性に直結します。本番環境では必ずバリデーションを設定し、機密情報がソースコードに含まれないよう注意してください。

参考リンク