Oteto Blogのロゴ

【NestJS】ConfigServiceをカスタムして.env(環境変数)をバリデーションする

困ってること

@nestjs/configを利用していると下記のように少し困ることがある。

  • 毎回configService.get<string>('xxx')という風にする場合、厳格な型安全性が無く変数名の補完も効かない
  • 想定した環境変数とその値が記述されているかの検証&保証ができてない
    • 環境毎に.envファイルを作成している場合、一部のファイルでのみ環境変数を設定し忘れるリスクもある

そこでConfigModuleをラップしてより便利にしつつ、.env(環境変数)のバリデーションもしてみる。

実装

0. 環境変数の設定

.env.developmentDATABASE_HOST=xxx
DATABASE_NAME=xxx
DATABASE_USER=xxx
DATABASE_PASSWORD=xxx
DATABASE_PORT=xxx

今回は上記のような環境変数を設定している前提で進める。

1. EnvModuleの実装

nest g module env
nest g service env
env.module.tsimport { Global, Module } from '@nestjs/common';
import { EnvService } from './env.service';
import { ConfigModule } from '@nestjs/config';
import { validate } from './env-validator';

@Global()
@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
  ],
  providers: [EnvService],
  exports: [EnvService],
})
export class EnvModule {}

グローバルに使いたいので@Globalデコレータも付与しておく。

2. EnvServiceの実装

env.service.tsimport { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

type DbConfig = {
  host?: string;
  name?: string;
  user?: string;
  password?: string;
  port?: number;
};

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

  isDev(): boolean {
    return this.configService.get<string>('NODE_ENV') === 'development';
  }

  get dbConfig(): DbConfig {
    return {
      host: this.configService.get<string>('DATABASE_HOST'),
      name: this.configService.get<string>('DATABASE_NAME'),
      user: this.configService.get<string>('DATABASE_USER'),
      password: this.configService.get<string>('DATABASE_PASSWORD'),
      port: this.configService.get<number>('DATABASE_PORT'),
    };
  }
}

ここでConfigServiceをDIし、設定した環境変数のgetterを追加する。

import { EnvService } from '../env/env.service';

@Injectable()
export class SampleService {
  constructor(
    private readonly envService: EnvService,
  ) {}

  sample() {
    const { host, name, user, password, port } = this.envService.dbConfig();
  }
}

あとは利用元でEnvServiceをDIすればよし。これでより型安全に環境変数を取り出せるようになった。

3. EnvValidatorの実装

touch env/env-validator.ts
npm i zod

環境変数をバリデーションするためのクラスを作成し、必要に応じてライブラリ(今回はzod)をインストールする。

env-validator.tsimport { z } from 'zod';

const NODE_ENVS = ['development', 'test', 'production'];
const zodString = z.string().min(1);

const envSchema = z.object({
  NODE_ENV: z.string().refine((v) => NODE_ENVS.includes(v)),
  DATABASE_HOST: zodString,
  DATABASE_NAME: zodString,
  DATABASE_USER: zodString,
  DATABASE_PASSWORD: zodString,
  DATABASE_PORT: z
    .string()
    .regex(/^\d+$/, { message: '数値の文字列を入力してください' }),
});

export function validate(
  config: Record<string, unknown>,
): Record<string, unknown> {
  envSchema.parse(config);
  return config;
}

ルールを記述の上validate関数を作成し、その中で判定を行う。

env.module.tsimport { Global, Module } from '@nestjs/common';
import { EnvService } from './env.service';
import { ConfigModule } from '@nestjs/config';
import { validate } from './env-validator';

@Global()
@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `.env.${process.env.NODE_ENV}`,
      validate,
    }),
  ],
  providers: [EnvService],
  exports: [EnvService],
})
export class EnvModule {}

あとはConfigModuleのオプションに先ほどのvalidate関数を指定すれば完了。

4. 動作確認

.env.developmentDATABASE_HOST=xxx

試しに適当な環境変数を削除してみる。

const error = new ZodError_1.ZodError(ctx.common.issues);
                              ^
ZodError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": [
      "DATABASE_HOST"
    ],
    "message": "Required"
  }
]

すると無事にバリデーションエラーが表示された。