For

2023.2.27

NestJSで開発環境と本番環境とテストでそれぞれに対応したenvファイルを読み込む方法

概要

NestJSでは カスタムenvファイルパスを定義する方法 が提供されていますが、機能的には複数のenvファイルを読み込んで同じ環境変数が複数のenvファイルで定義されていれば最初に読み込んだものに定義された値を優先するというものなので、この記事ではcross-envと@nestjs/configを使って開発環境と本番環境とテストでそれぞれに対応したenvファイルを読み込む方法を公開します。

今回の内容は サンプルリポジトリ に公開しています。

手順

事前準備

もし @nestjs/cli がグローバルにインストールされていなかったらインストールします。

bash_____terminal_____$ npm i -g @nestjs/cli


NestJSアプリケーションを作成

任意のディレクトリにNestJSアプリケーションを作成します。

bash_____terminal_____$ nest new .


今回はyarnを使用して進めていきます。

envファイルを作成

text_____src/envs/development.env_____ENV_FILE_NAME=development.env


text_____src/envs/production.env_____ENV_FILE_NAME=production.env


text_____src/envs/test.env_____ENV_FILE_NAME=test.env


nest-cli.jsonを更新

JSON_____nest-cli.json_____{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "assets": ["envs/*"]
  }
}


NODE_ENVに対応したenvファイルパスを返す関数

TypeScript_____src/utils/getEnvPath.ts_____import { existsSync } from 'fs';
import { resolve } from 'path';

export const getEnvFilePath = (envFilesDirectory: string) => {
  const env = process.env.NODE_ENV;
  const fileName = env ? `${env}.env` : 'development.env';
  const filePath = resolve(`${envFilesDirectory}/${fileName}`);
  return existsSync(filePath) ? filePath : resolve(`${envFilesDirectory}/.env`);
};


関連ファイルを更新

bash_____terminal_____$ yarn add @nestjs/config


TypeScript_____src/app.module.ts_____import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { getEnvFilePath } from './utils/getEnvFilePath';

const envFilePath: string = getEnvFilePath(`${__dirname}/envs`);

@Module({
  imports: [ConfigModule.forRoot({ envFilePath, isGlobal: true })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}


TypeScript_____src/app.service.ts_____import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getEnvFileName(): string {
    return `ENV_FILE_NAME: ${process.env.ENV_FILE_NAME ?? 'Undefined'}`;
  }
}


TypeScript_____src/app.controller.ts_____import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getEnvFileName(): string {
    return this.appService.getEnvFileName();
  }
}


TypeScript_____src/app.controller.spec.ts_____import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { ConfigModule } from '@nestjs/config';
import { AppService } from './app.service';
import { getEnvFilePath } from './utils/getEnvFilePath';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const envFilePath: string = getEnvFilePath(`${__dirname}/envs`);

    const app: TestingModule = await Test.createTestingModule({
      imports: [ConfigModule.forRoot({ envFilePath, isGlobal: true })],
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "ENV_FILE_NAME: test.env"', () => {
      expect(appController.getEnvFileName()).toBe('ENV_FILE_NAME: test.env');
    });
  });
});


cross-envを追加してscriptsを更新

bash_____terminal_____$ yarn add -D cross-env


JSON_____package.json_____{
  // ...,
  "scripts": {
    "start:dev": "cross-env NODE_ENV=development nest start --watch",
    "start:prod": "cross-env NODE_ENV=production node dist/main",
    "test": "cross-env NODE_ENV=test jest",
    // ...,
  },
  // ...,
}


読み込まれているenvファイルを確認

開発環境

開発環境にするため yarn start:dev を実行したあとcurlで環境変数を確認してみます。

text_____terminal_____curl http://localhost:3000/  
ENV_FILE_NAME: development.env%


本番環境

本番環境にするため yarn start:prod を実行したあとcurlで環境変数を確認してみます。

text_____terminal_____curl http://localhost:3000/  
ENV_FILE_NAME: production.env%


テスト

最後にテストを実行してみます。

text_____terminal_____$ yarn test
yarn run v1.22.19
$ cross-env NODE_ENV=test jest
 PASS  src/app.controller.spec.ts
  AppController
    root
      ✓ should return "ENV_FILE_NAME: test.env" (16 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.353 s, estimated 3 s
Ran all test suites.
✨  Done in 2.96s.


まとめ

上記の方法で開発環境、本番環境、テストでそれぞれに対応したenvファイルを読み込めたことが確認できました。
NestJSが提供している機能よりもう少しソリッドなアプローチとなるので、お好みで使ってみてください。