开发 NestJS 应用时,配置管理是最基础也最容易混乱的一环。端口号、数据库地址、Redis 密码、业务白名单等,如果散落在代码各处的 process.env 中,不仅难以维护,还会导致类型丢失、环境切换麻烦。本文梳理了从直接读环境变量到终极 registerAs 命名空间加 Joi 校验的完整方案,包含代码实现和踩坑记录。
- // 最原始的写法,字段名靠记忆,类型全是 string | undefined
- const port = process.env.PORT || 3000;
- const dbHost = process.env.DATABASE_HOST;
复制代码
一、官方标配:@nestjs/config 模块
@nestjs/config 底层依赖 dotenv,是主流的标准入口。安装后可在根模块引入 ConfigModule 并调用 forRoot()。
- import { Module } from '@nestjs/common';
- import { ConfigModule } from '@nestjs/config';
- @Module({
- imports: [
- ConfigModule.forRoot({
- isGlobal: true, // 设为全局,其他模块无需重复 import
- }),
- ],
- })
- export class AppModule {}
复制代码
踩坑:系统环境变量优先级高于 .env 文件中的同名 key。线上排查时要注意。
多环境可传数组给 envFilePath:
- ConfigModule.forRoot({
- envFilePath: [
- `.env.${process.env.NODE_ENV}.local`, // 优先级最高
- `.env.${process.env.NODE_ENV}`,
- '.env', // 兜底
- ],
- isGlobal: true,
- });
复制代码
注意:数组越靠前优先级越高,重复 key 只会取第一个找到的值。
二、进阶:配置工厂函数结构化对象
将零散环境变量组装成有结构、有默认值的对象:
- // config/database.config.ts
- export default () => ({
- port: parseInt(process.env.PORT, 10) || 3000,
- database: {
- host: process.env.DATABASE_HOST || 'localhost',
- port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
- user: process.env.DATABASE_USER,
- password: process.env.DATABASE_PASSWORD,
- },
- });
复制代码
通过 load 属性加载:
- import databaseConfig from './config/database.config';
- ConfigModule.forRoot({
- isGlobal: true,
- load: [databaseConfig],
- });
复制代码
使用 ConfigService.get 获取嵌套属性:
- const dbHost = this.configService.get<string>('database.host');
- const dbPort = this.configService.get<number>('database.port');
复制代码
但字符串路径没有类型提示,写错只能运行时暴露。
三、终极利器:命名空间(Namespace)registerAs
registerAs() 返回的对象带有 .KEY 属性,可直接用于依赖注入,获得完整类型推导。
- // config/database.config.ts
- import { registerAs } from '@nestjs/config';
- export default registerAs('database', () => ({
- host: process.env.DATABASE_HOST || 'localhost',
- port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
- name: process.env.DATABASE_NAME,
- }));
- // config/redis.config.ts
- export default registerAs('redis', () => ({
- host: process.env.REDIS_HOST || 'localhost',
- port: parseInt(process.env.REDIS_PORT, 10) || 6379,
- }));
复制代码
在根模块统一加载:
- ConfigModule.forRoot({
- isGlobal: true,
- load: [databaseConfig, redisConfig],
- });
复制代码
在 Service 中注入整个配置对象:
- import { Inject, Injectable } from '@nestjs/common';
- import { ConfigType } from '@nestjs/config';
- import databaseConfig from './config/database.config';
- @Injectable()
- export class DatabaseService {
- constructor(
- @Inject(databaseConfig.KEY)
- private readonly dbConfig: ConfigType<typeof databaseConfig>,
- ) {}
- getConnection() {
- return `${this.dbConfig.host}:${this.dbConfig.port}/${this.dbConfig.name}`;
- }
- }
复制代码
ConfigType<typeof databaseConfig> 自动推导工厂函数返回值类型,无需手动写 Interface。
.asProvider() 方法可简化第三方模块异步配置:
- TypeOrmModule.forRootAsync(databaseConfig.asProvider())
复制代码
这等价于手写 imports/inject/useFactory,且自动保证初始化顺序。
四、YAML / JSON 配置
YAML 天然支持数字、布尔类型,无需手动 parseInt。需要安装 js-yaml 和类型定义:
- import { readFileSync } from 'node:fs';
- import { join } from 'node:path';
- import * as yaml from 'js-yaml';
- export default () => {
- return yaml.load(
- readFileSync(join(__dirname, 'config.yaml'), 'utf8'),
- ) as Record<string, any>;
- };
复制代码
踩坑:Nest CLI 默认不拷贝非 TS 文件,须在 nest-cli.json 中配置 assets。
- {
- "compilerOptions": {
- "assets": [{ "include": "../config/*.yaml", "outDir": "./dist/config" }]
- }
- }
复制代码
远端配置中心也可通过异步工厂函数接入:
- export default registerAs('secret', async () => {
- const data = await fetchFromConfigCenter('/api/config/secret');
- return { apiKey: data.apiKey, jwtSecret: data.jwtSecret };
- });
复制代码
五、配置校验
没有校验的配置是定时炸弹。NestJS 提供 Joi 和 class-validator 两种方案。
Joi 校验示例:
- import * as Joi from 'joi';
- ConfigModule.forRoot({
- validationSchema: Joi.object({
- NODE_ENV: Joi.string().valid('dev', 'prod').default('dev'),
- PORT: Joi.number().default(3000),
- DATABASE_HOST: Joi.string().required(),
- }),
- validationOptions: {
- allowUnknown: true,
- abortEarly: false,
- },
- });
复制代码
注意:自定义 validationOptions 会覆盖 @nestjs/config 默认值。建议显式设置 allowUnknown 和 abortEarly。
class-validator 方案:
- import { plainToInstance } from 'class-transformer';
- import { IsNumber, IsString, validateSync } from 'class-validator';
- class EnvVariables {
- @IsNumber() PORT: number;
- @IsString() DATABASE_HOST: string;
- }
- export function validate(config: Record<string, unknown>) {
- const validated = plainToInstance(EnvVariables, config, { enableImplicitConversion: true });
- const errors = validateSync(validated, { skipMissingProperties: false });
- if (errors.length > 0) throw new Error(errors.toString());
- return validated;
- }
复制代码
六、特殊场景
6.1 局部注册(forFeature)的生命周期坑
- @Module({
- imports: [ConfigModule.forFeature(databaseConfig)],
- })
- export class DatabaseModule {}
复制代码
不要在构造函数中读取配置,改用属性注入并在 onModuleInit 钩子里读取:
- import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
- @Injectable()
- export class DatabaseService implements OnModuleInit {
- @Inject(databaseConfig.KEY)
- private readonly dbConfig: ConfigType<typeof databaseConfig>;
- onModuleInit() {
- console.log('db host:', this.dbConfig.host); // ❌ 安全
- }
- }
复制代码
6.2 配置之间的依赖链
TypeORM 等模块需要配置就绪后才能初始化。.asProvider() 自动保证了顺序。手写 useFactory 时务必写 imports: [ConfigModule]。
配置工厂之间不能互相依赖,所有工厂应平级地从 process.env 取原始值。
6.3 在 main.ts 中获取配置
- async function bootstrap() {
- const app = await NestFactory.create(AppModule);
- const configService = app.get(ConfigService);
- const port = configService.get<number>('PORT') ?? 3000;
- await app.listen(port);
- }
复制代码
七、总结与推荐
经过对比,registerAs() 命名空间 + isGlobal: true + 启动时 Joi 强校验是最推荐的实战组合。配置在哪定义,就在哪描述结构;业务代码只管用,不关心底层来源。这样即使将来迁移到云端配置中心,业务代码也无需改动。
注:本文涉及的代码均基于 TypeScript 4.1+ 和 @nestjs/config 最新稳定版。实际使用时请根据项目版本调整包版本。 |