配置管理
本教程共 47 篇 · 第 24 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:学会用 @nestjs/config 管理应用配置,包括环境变量加载、自定义配置文件、配置验证和命名空间,让应用在不同环境下灵活切换。
你的应用里肯定有不少”会变的东西”:数据库地址、API 密钥、端口号、第三方服务地址……这些东西不能硬编码在代码里。
为什么?因为开发环境、测试环境、生产环境的配置都不一样。你把数据库密码写死在代码里,推到 Git 上,全世界都看到了。
所以我们需要一个正经的配置管理方案。
基本用法:加载 .env 文件
安装
npm install @nestjs/config
@nestjs/config 底层用的是 dotenv 这个包。它做的事很简单:读取 .env 文件里的键值对,塞到 process.env 里。
创建 .env 文件
在项目根目录创建一个 .env 文件:
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=admin
DATABASE_PASSWORD=secret123
JWT_SECRET=my-super-secret-key
PORT=3000
Warning
.env文件包含敏感信息,一定要加到.gitignore里。永远不要把密钥提交到代码仓库。
注册 ConfigModule
在根模块中导入 ConfigModule:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
forRoot() 会自动读取项目根目录下的 .env 文件,把里面的变量合并到 process.env。
在代码中读取配置
通过注入 ConfigService 来读取配置值:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class DatabaseService {
constructor(private configService: ConfigService) {}
connect() {
const host = this.configService.get<string>('DATABASE_HOST');
const port = this.configService.get<number>('DATABASE_PORT');
console.log(`Connecting to ${host}:${port}`);
}
}
get() 方法支持泛型,可以指定返回类型。还支持设置默认值:
// 如果 PORT 不存在,返回 3000
const port = this.configService.get<number>('PORT', 3000);
Tip如果你在很多模块中都要用 ConfigService,可以把 ConfigModule 设为全局模块,这样就不用每个模块都 import 了:
ConfigModule.forRoot({ isGlobal: true, })
自定义配置文件
项目大了以后,配置项会越来越多。全塞在一个 .env 文件里不好管理。更好的方式是按功能拆分配置文件。
按功能拆分
// config/database.config.ts
export default () => ({
database: {
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
username: process.env.DATABASE_USER || 'admin',
password: process.env.DATABASE_PASSWORD,
name: process.env.DATABASE_NAME || 'mydb',
},
});
// config/app.config.ts
export default () => ({
app: {
port: parseInt(process.env.PORT, 10) || 3000,
name: process.env.APP_NAME || 'my-app',
env: process.env.NODE_ENV || 'development',
},
});
加载配置文件
import databaseConfig from './config/database.config';
import appConfig from './config/app.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [appConfig, databaseConfig],
}),
],
})
export class AppModule {}
读取嵌套配置
用点号语法读取嵌套的配置值:
// 读取 database.host
const dbHost = this.configService.get<string>('database.host');
// 读取整个 database 对象
const dbConfig = this.configService.get('database');
// dbConfig.host, dbConfig.port, ...
命名空间配置:registerAs
配置文件多了以后,可以用 registerAs 给每个配置文件加一个”命名空间”,读取的时候更清晰。
// 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,
username: process.env.DATABASE_USER || 'admin',
password: process.env.DATABASE_PASSWORD,
}));
加载方式和之前一样:
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
}),
],
})
export class AppModule {}
读取时用命名空间做前缀:
// 方式一:用点号语法
const dbHost = this.configService.get<string>('database.host');
// 方式二:直接注入命名空间(推荐,有类型提示)
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {}
// 使用
this.dbConfig.host // string
this.dbConfig.port // number
Tip方式二的好处是有 TypeScript 类型提示。
ConfigType会自动推导出配置对象的类型,写代码时 IDE 能自动补全。
命名空间配合其他模块使用
命名空间配置可以直接传给其他模块的 forRootAsync,非常方便:
import databaseConfig from './config/database.config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRootAsync(databaseConfig.asProvider()),
],
})
export class AppModule {}
asProvider() 会自动把命名空间配置转成 forRootAsync 需要的格式。
配置验证
应用启动时如果关键配置缺失,与其运行到一半才报错,不如直接在启动阶段就拦住。
方式一:用 Joi 验证
安装 Joi:
npm install joi
定义验证规则:
// app.module.ts
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000),
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().default(5432),
DATABASE_PASSWORD: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
}),
}),
],
})
export class AppModule {}
如果验证失败——比如 DATABASE_HOST 没配——应用会直接启动失败,并抛出验证错误。
Note
required()标记的变量必须存在,否则启动报错。没标记的变量是可选的,配合default()可以给默认值。
方式二:用 class-validator 验证
如果你更喜欢装饰器的风格,可以用 class-validator + class-transformer:
// env.validation.ts
import { plainToInstance } from 'class-transformer';
import { IsEnum, IsNumber, IsString, Min, Max, validateSync } from 'class-validator';
enum Environment {
Development = 'development',
Production = 'production',
Test = 'test',
}
class EnvironmentVariables {
@IsEnum(Environment)
NODE_ENV: Environment;
@IsNumber()
@Min(0)
@Max(65535)
PORT: number;
@IsString()
DATABASE_HOST: string;
@IsString()
DATABASE_PASSWORD: string;
}
export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToInstance(
EnvironmentVariables,
config,
{ enableImplicitConversion: true },
);
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
然后在 ConfigModule 中使用:
import { validate } from './env.validation';
@Module({
imports: [
ConfigModule.forRoot({
validate,
}),
],
})
export class AppModule {}
Tip两种验证方式选一种就行。Joi 更简洁,class-validator 适合已经在用装饰器风格的项目。
多环境配置
开发、测试、生产环境的配置不一样。常见的做法是为每个环境准备一个 .env 文件:
.env # 公共配置(所有环境共享)
.env.development # 开发环境
.env.production # 生产环境
.env.test # 测试环境
根据 NODE_ENV 加载不同文件
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
}),
],
})
export class AppModule {}
也可以指定多个文件路径,前面的优先级更高:
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env',
],
})
在 main.ts 中使用配置
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
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);
console.log(`Application is running on port ${port}`);
}
bootstrap();
也可以用 Node.js 20+ 的 --env-file 选项,在应用启动前就加载环境变量:
nest start --env-file .env
自定义 Getter 方法
configService.get('SOME_KEY') 每次都要写字符串 key,容易拼错。可以封装一层自定义的 ConfigService:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AppConfigService {
constructor(private configService: ConfigService) {}
get port(): number {
return this.configService.get<number>('PORT', 3000);
}
get databaseHost(): string {
return this.configService.get<string>('DATABASE_HOST');
}
get databasePort(): number {
return this.configService.get<number>('DATABASE_PORT', 5432);
}
get jwtSecret(): string {
return this.configService.get<string>('JWT_SECRET');
}
get isProduction(): boolean {
return this.configService.get<string>('NODE_ENV') === 'production';
}
}
使用的时候就很清晰了:
constructor(private appConfig: AppConfigService) {}
connect() {
const host = this.appConfig.databaseHost;
const port = this.appConfig.databasePort;
}
Tip封装自定义 ConfigService 的好处:类型安全、IDE 自动补全、集中管理配置访问逻辑。项目一大,这个封装就很有价值。
条件加载模块
有时候你想根据环境变量决定是否加载某个模块。比如开发环境加载 MockModule,生产环境加载真实的 ServiceModule。
import { ConditionalModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
ConditionalModule.registerWhen(MockModule, 'USE_MOCK'),
],
})
export class AppModule {}
当 .env 里 USE_MOCK=true 时加载 MockModule,USE_MOCK=false 或不设置时不加载。
也可以传一个函数做更复杂的判断:
ConditionalModule.registerWhen(
CacheModule,
(env: NodeJS.ProcessEnv) => env.NODE_ENV === 'production',
)
变量展开
.env 文件支持变量引用,避免重复写相同的值:
APP_DOMAIN=mywebsite.com
SUPPORT_EMAIL=support@${APP_DOMAIN}
API_URL=https://api.${APP_DOMAIN}
需要开启 expandVariables 选项:
ConfigModule.forRoot({
expandVariables: true,
})
这样 SUPPORT_EMAIL 会自动解析为 support@mywebsite.com。
部分注册
大项目中,不同功能模块可能有自己的配置文件。不用全堆在 AppModule 里,可以在各自的模块中注册:
// database.module.ts
import databaseConfig from './config/database.config';
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
Note
forFeature()在模块初始化时执行。如果你在构造函数里访问它加载的配置,可能还没准备好。保险起见,在onModuleInit生命周期钩子里访问。
踩坑经验
1. .env 文件中的值不需要引号
# 正确
DATABASE_HOST=localhost
# 也行,但不必要
DATABASE_HOST="localhost"
dotenv 会自动去掉首尾的引号。但如果值里有空格,就需要引号了。
2. process.env 的值全是字符串
.env 文件里写的 PORT=3000,读出来是字符串 "3000",不是数字。需要手动转换:
const port = parseInt(this.configService.get('PORT'), 10);
这就是为什么在自定义配置文件里要用 parseInt() 做类型转换。
3. 运行时环境变量优先级高于 .env 文件
如果你同时设了系统环境变量 PORT=4000 和 .env 文件里 PORT=3000,最终取到的是 4000。系统环境变量优先级更高。
4. 别忘了缓存
ConfigService.get() 每次都会访问 process.env,有一定性能开销。如果频繁读取,开启缓存:
ConfigModule.forRoot({
cache: true,
})
5. 配置文件不会被 validationSchema 验证
自定义配置文件(通过 load 加载的)不会自动被 validationSchema 验证。如果需要验证自定义配置,在配置文件工厂函数里自己做。
小结
关键知识点回顾:
ConfigModule.forRoot()加载.env文件到环境变量ConfigService注入后读取配置值registerAs定义命名空间配置,用asProvider注册Joi做配置验证,启动时就能发现配置错误- 多环境配置用不同的
.env文件,通过envFilePath指定
下一章我们聊聊数据验证,看看怎么保证请求数据的正确性。