アレについて記す

Nest(TypeScript)で遊んでみる 〜Configuration編〜

Posted on September 18, 2018 at 01:30 (JST)

今回は環境別設定を .env ファイルに定義し、Nestアプリケーションで利用する方法について記載する。

  1. ライブラリをインストール
  2. 環境別設定を読み込む
  3. TypeORMの設定として利用する

作成したコード [ nest-angular-sample: works/08_configuration ]

動作環境

OS: macOS High Sierra ver. 10.13.4
Nodejs: v8.10.0
npm: 5.6.0
nest(core): 5.3.6
Docker: 18.06.1-ce

1. ライブラリをインストール

公式ドキュメント を参考に進める。

今回はライブラリを2つインストールする。

$ npm install --save dotenv joi
$ npm install --save-dev @types/dotenv @types/joi

dotenv

.env ファイルに記載した設定をobjectへと変換するのに使用する。
dotenv (Github)

joi

JavaScriptのobjectの検証に利用する。
joi (Github)

2. 環境別設定を読み込む

2-1. .envファイルを配備する

下記3点ファイルを用意する

  • env/dev.env
  • env/prod.env
  • env/test.env

内容は環境にあわせ適宜変更する

DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=user
DATABASE_PASSWORD=password
TYPEORM_SYNCHRONIZE=false

2-2. ConfigServiceを用意する

[src/config/config.service.ts]

import * as dotenv from 'dotenv';
import * as Joi from 'joi';
import * as fs from 'fs';

export interface EnvConfig {
  [key: string]: string;
}

export interface OrmEnv {
  host: string;
  port: number;
  user: string;
  password: string;
  synchronize: boolean;
}

export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(filePath: string) {
    const config = dotenv.parse(fs.readFileSync(filePath));
    const merged = Object.assign({}, config, process.env);
    this.envConfig = this.validateInput(merged);
  }

  private validateInput(envConfig: EnvConfig): EnvConfig {
    const envVarsSchema: Joi.ObjectSchema = Joi.object({
      ENV_NAME: Joi.string()
        .valid(['dev', 'prod', 'test'])
        .default('dev'),
      DATABASE_HOST: Joi.string().required(),
      DATABASE_PORT: Joi.number().required(),
      DATABASE_USER: Joi.string().required(),
      DATABASE_PASSWORD: Joi.string().required(),
      TYPEORM_SYNCHRONIZE: Joi.boolean(),
    });

    const { error, value: validatedEnvConfig } = Joi.validate(
      envConfig,
      envVarsSchema,
      { stripUnknown: true },
    );
    if (error) {
      throw new Error(`Config validation error: ${error.message}`);
    }
    return validatedEnvConfig;
  }

  get typeOrm(): OrmEnv {
    return {
      host: this.envConfig.DATABASE_HOST,
      user: this.envConfig.DATABASE_USER,
      port: parseInt(this.envConfig.DATABASE_PORT, 10),
      password: this.envConfig.DATABASE_PASSWORD,
      synchronize: Boolean(this.envConfig.TYPEORM_SYNCHRONIZE),
    };
  }
}

ポイントは下記になる

  • dotenv.parse でファイルの内容をobject化
  • コマンドライン引数や .bash_profile にて環境変数の指定があった場合は値を上書きする。(本番パスワード等を.envファイルに書かなくてすむ)
  • Joi.ObjectSchema に沿った入力値となっているか検証できる。
  • Joi.validate にオプション(stripUnknown: true)を設定することでスキーマに定義した値以外を削ぎ落とすことができる。
  • dotenvで読み込んだ値やprocess.envの値はstringなので適宜Castする必要がある。

2-3. ConfigModuleを用意する

[src/config/config.module.ts]

import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({
  providers: [
    {
      provide: ConfigService,
      useValue: new ConfigService(`env/${process.env.ENV_NAME}.env`),
    },
  ],
  exports: [ConfigService],
})
export class ConfigModule {}

公式ドキュメントでは NODE_ENV という値で読み込むファイルを変えている。
今回別の環境変数を用意しているのは、フレームワークで想定しているパターン以上に環境が増えることを想定しているからである。
NODE_ENV はフレームワーク内で制御用に使用されていることが多い。
下手に独自環境名を追加すると予期せぬエラーにつながるため、開発者が制御に用いるための別の変数を用意している。

3. TypeORMの設定として利用する

3-1. TypeORM用の設定Providorを用意する

[src/config/typeorm.config.ts]

import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { TaskEntity } from '../tasks/entities/task.entity';
import { Injectable } from '@nestjs/common';
import { ConfigService } from './config.service';

@Injectable()
export class TypeOrmConfig implements TypeOrmOptionsFactory {
  constructor(private readonly config: ConfigService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: this.config.typeOrm.host,
      port: 3306,
      username: this.config.typeOrm.user,
      password: this.config.typeOrm.password,
      database: 'test_db',
      entities: [TaskEntity],
      synchronize: this.config.typeOrm.synchronize,
    };
  }
}

※ 前回用意したdb.config.tsは不要となるので削除。

3-2. ConfigModuleに追加

import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { TypeOrmConfig } from './typeorm.config';

@Module({
  providers: [
    {
      provide: ConfigService,
      useValue: new ConfigService(`env/${process.env.ENV_NAME}.env`),
    },
    TypeOrmConfig,
  ],
  exports: [ConfigService, TypeOrmConfig],
})
export class ConfigModule {}

3-3. AppModule変更

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';
import { LoggerModule } from './logger/logger.module';
import { SamplesModule } from './samples/samples.module';
import { TypeOrmConfig } from './config/typeorm.config';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useExisting: TypeOrmConfig,
    }),
    LoggerModule,
    TasksModule,
    SamplesModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

TypeOrmModule.forRootAsync を使って設定を読み込むのがポイント。
AppModule初期化時にはまだ、TypeOrmConfigが初期化されていないため非同期で読み込む必要がある。


以上。


参考URL