NestJSでConfigModuleを使いやすくするtips
nestjsのConfigModule
便利ですね。今回はこの便利なConfigModule
を更に便利にしようと思います。おさらいから入るので、実際の実装が知りたい方は途中まで飛ばしてください。
まずはおさらい
環境変数を登録する側
まずはモジュールをインストールします。
npm i --save @nestjs/config
ConfigServiceを使いたいモジュールでインポートします。forRoot()
で細かな設定ができます。デフォルトでは.env
ファイルを読み込んでくれます。
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
.env
ではないファルを指定したり、
ConfigModule.forRoot({
envFilePath: '.development.env',
});
グローバルにしたりもできます。
ConfigModule.forRoot({
isGlobal: true,
});
また別ファイルで環境をjson
にまとめ
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432
}
});
それを指定して使うことも可能です。
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
],
})
export class AppModule {}
環境変数を使う側
ConfigModule
がグローバルに登録されていない場合は、使いたい側のモジュールにConfigModule
をインポートします。グローバルに登録されている場合は不要です。
@Module({
imports: [ConfigModule],
// ...
})
あとは、使いたい側のサービスなどに注入します。
import { ConfigService } from '@nestjs/config';
// 省略
constructor(private configService: ConfigService) {}
こうすることで、そのサービス内部で環境変数を取得できます。
// get an environment variable
const dbUser = this.configService.get<string>('DATABASE_USER');
// get a custom configuration value
const dbHost = this.configService.get<string>('database.host');
少し戻って、ConfigService
を注入する際に、ジェネリクスを指定できます。ConfigService<EnvironmentVariables>
の部分に注目してください。
interface EnvironmentVariables {
PORT: number;
TIMEOUT: string;
}
// somewhere in the code
constructor(private configService: ConfigService<EnvironmentVariables>) {
const port = this.configService.get('PORT');
// TypeScript Error: this is invalid as the URL property is not defined in EnvironmentVariables
const url = this.configService.get('URL');
}
こうすると、なんとthis.configService.get()
返り値に型がききます。またConfigService<EnvironmentVariables, true>
と2つ目のジェネリクスを指定すると、返り値の型からundefined
が取り除かれます。
constructor(private configService: ConfigService<EnvironmentVariables>) {
// portは number | undefined 型
const port = this.configService.get('PORT');
}
constructor(private configService: ConfigService<EnvironmentVariables, true>) {
// portは number型
const port = this.configService.get('PORT');
}
そしてこれは一番驚いたのですが、this.configService.get('PORT', { infer: true })
のように{ infer: true }
をつけると、ドットで値にアクセスできるようになります。型推論も効きます。TypeScript v4.1 から可能になったみたいです。stackoverflow。
type DBConfig = { database: { host: string } }
constructor(private configService: ConfigService<DBConfig>) {
const dbHost = this.configService.get('database.host', { infer: true });
}
長くなりましたがおさらいは以上です。使ってて次のことを実現したいなと思いました。
- 常に
{ infer: true }
にしたい - configurationファイルを作成して、環境変数にIntなどの変換処理をほどこしてからConfigModuleに渡したい。
- 常にConfigServiceのジェネリクスの第1引数に、先程作ったconfigurationファイルをもとに型を生成して渡したい。
- 常にConfigServiceのジェネリクスの第2引数をtrueにしたい。
- 上記を毎回設定するのは大変。ConfigModuleを拡張して、これらがデフォルトで指定されているようにしたい。
独自のConfigModuleを作る
目標
import { AppConfigService } from '../config/config.service.ts';
HogeService {
constructor(@Inject('AppConfigService') private config: AppConfigService){
// string型が返ってくる。
const dbHost = this.config.get('database.host');
}
}
実装
configurationファイル作成
こちらのベストプラクティスに従って、環境変数をまとめていきます。
const convertOrDefault = (
raw: string | undefined,
defaultValue: string,
): string => {
if (!raw) return defaultValue;
return raw;
};
const convertIntOrDefault = (
raw: string | undefined,
defaultValue: number,
): number => {
if (!raw) return defaultValue;
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) return defaultValue;
return parsed;
};
const convertBooleanOrDefault = (
raw: string | undefined,
defaultValue: boolean,
): boolean => {
if (!raw) return defaultValue;
if (raw !== 'true' && raw !== 'false') return defaultValue;
if (raw === 'true') return true;
if (raw === 'false') return false;
return false;
};
export const generateAppConfig = () => {
if (
!process.env.DB_USER_NAME ||
!process.env.DB_PASSWORD ||
!process.env.DB_NAME ||
!process.env.DB_HOST
) {
console.error(
'DB_USER_NAME or DB_PASSWORD or DB_NAME or DB_HOST environmental variable is missing!',
);
process.exit();
}
return {
database: {
// [必須] DBユーザー名
username: process.env.DB_USER_NAME,
// [必須] DBパスワード
password: process.env.DB_PASSWORD,
// [必須] データベース名
database: process.env.DB_NAME,
// [必須] DBのホスト名
host: process.env.DB_HOST,
// データーベースの種類。
dialect: convertOrDefault(process.env.DB_KIND, 'postgres'),
// SQLを標準ログ出力するかどうか
logging: convertBooleanOrDefault(process.env.DB_SEQUELIZE_LOGGING, false),
},
};
};
export type AppConfig = ReturnType<typeof generateAppConfig>;
やっていることは、
- 必須の環境変数がなければ、サーバーを停止
- デフォルトの環境変数を指定
- 数字や真偽値は変換
です。
最後にこの関数の返り値から型情報を抽出しています。これをConfigSevice<型情報>
に渡してあげます。
Configモジュール作成
.env
ファイルの読み込みを停止し、configuration
ファイルを指定しています。.env
を使わなくても良い開発方法に関してはこちらを参照してください。
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppConfigService } from './config.service';
import { generateAppConfig } from './configuration';
// グローバルにする
@Global()
@Module({
imports: [
ConfigModule.forRoot({
// .envよみこまない
ignoreEnvFile: true,
// 設定ファイル指定
load: [generateAppConfig],
}),
],
providers: [
{
provide: 'AppConfigService',
useClass: AppConfigService,
},
],
exports: ['AppConfigService'],
})
export class AppConfigModule {}
configサービスの作成
これが一番試行錯誤しました。extends
したメソッドを呼ぶときの引数の型が難しかったorz。
しかし、これで最初に思い描いていたことがすべて達成できました!!
import { Injectable } from '@nestjs/common';
import { ConfigService, Path, PathValue } from '@nestjs/config';
import { AppConfig } from './configuration';
// 参考: https://stackoverflow.com/questions/68411508/typescript-type-safe-string-with-dot-notation-for-query-nested-object
@Injectable()
export class AppConfigService extends ConfigService<AppConfig, true> {
// eslint-disable-next-line no-use-before-define
get<P extends Path<T>, T = AppConfig>(arg: P): PathValue<T, P> {
return super.get<T, P, PathValue<T, P>>(arg, { infer: true });
}
}
あとは環境変数を使いたいサービスからこのAppConfigService
を呼び出してあげるだけです。
import { AppConfigService } from '../config/config.service.ts';
HogeService {
constructor(@Inject('AppConfigService') private config: AppConfigService){
// string型が返ってくる。
const dbHost = this.config.get('database.host');
}
}
Discussion