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

困ってること

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

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

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

実装

0. 環境変数の設定

.env.development
DATABASE_HOST=xxx
DATABASE_NAME=xxx
DATABASE_USER=xxx
DATABASE_PASSWORD=xxx
DATABASE_PORT=xxx

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

1. EnvModuleの実装

Terminal window
nest g module env
nest g service env
env.module.ts
import { 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.ts
import { 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の実装

Terminal window
touch env/env-validator.ts
npm i zod

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

env-validator.ts
import { 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.ts
import { 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.development
DATABASE_HOST=xxx

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

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

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

1

参考
  1. NestJSのConfigurationプラクティス:.envと環境変数, バリデーション, 独自の環境変数読み出しサービス