第 12 章:配置管理
学习目标
- 用 @nestjs/config 管理配置
- 区分环境变量与配置文件
- 校验配置项
- 避开 3 个配置坑
一、为什么需要 ConfigModule
直接 process.env.DB_URL 散落各处,难统一、难校验。@nestjs/config 提供:
- 集中加载
.env - 类型安全
- Schema 校验
- 命名空间(分模块配置)
二、安装与基础用法
bash
pnpm add @nestjs/configtypescript
// app.module.ts
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}建 .env:
env
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=admin
DB_PASS=secret
JWT_SECRET=mysecret.env.example 给团队参考(不带真密码):
env
PORT=3000
DB_HOST=
DB_PORT=5432
DB_USER=
DB_PASS=
JWT_SECRET=注入:
typescript
@Injectable()
export class AppService {
constructor(private readonly config: ConfigService) {}
getPort(): number {
return this.config.get<number>('PORT', 3000); // 默认 3000
}
}⚠️ 坑 1:
.env提交到 git → 泄露密码 → 加进.gitignore,只提交.env.example。
三、读取嵌套配置
typescript
// config/app.config.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
db: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10) || 5432,
},
});加载:
typescript
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig],
});读取:
typescript
this.config.get('db.host'); // 字符串 'localhost'
this.config.get('db.port'); // 数字 5432四、命名空间
typescript
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10) || 5432,
user: process.env.DB_USER,
pass: process.env.DB_PASS,
}));加载 + 读取:
typescript
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig],
});
// 注入
constructor(
@Inject('database') private dbCfg: ConfigType<typeof databaseConfig>,
) {}
this.dbCfg.host; // 类型安全registerAs + ConfigType 详解:
为什么需要命名空间? 把不同业务的配置分组,避免字段混在一起:
typescript
// ❌ 扁平 —— 字段容易撞名
{ dbHost: '...', dbPort: 5432, jwtSecret: 'xxx' }
// ✅ 命名空间 —— 按业务分组
{ database: { host, port, user, pass }, jwt: { secret, expires } }完整使用:
typescript
// 1️⃣ 定义多个命名空间配置
// config/database.config.ts
export default registerAs('database', () => ({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10) || 5432,
}));
// config/jwt.config.ts
export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES || '1h',
}));
// 2️⃣ 加载所有
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, jwtConfig],
cache: true, // 启动时读一次,缓存到内存
});
// 3️⃣ 注入 + 类型安全
@Injectable()
export class UsersService {
constructor(
@Inject(databaseConfig.KEY) private dbCfg: ConfigType<typeof databaseConfig>,
@Inject(jwtConfig.KEY) private jwtCfg: ConfigType<typeof jwtConfig>,
) {}
connect() {
console.log(this.dbCfg.host); // 类型安全,IDE 补全
}
}3 个关键概念:
| 概念 | 作用 | 说明 |
|---|---|---|
registerAs('名字', factory) | 分组 | 把配置放到 名字 这个命名空间下 |
ConfigType<typeof xxxConfig> | 类型安全 | 自动推导配置的类型,有补全 |
xxxConfig.KEY | DI token | 就是字符串 '名字',方便 @Inject() 用 |
为什么用 ConfigType?
typescript
// ❌ @Inject('database') private cfg: any;
this.cfg.htos; // ← 拼写错误,运行时才报 undefined
// ✅ @Inject(databaseConfig.KEY) private cfg: ConfigType<typeof databaseConfig>;
this.cfg.host; // ← 拼写错误,IDE 直接红线3 种注入方式对比:
| 方式 | 特点 |
|---|---|
@Inject('database') cfg: any | 简单,没类型 |
@Inject(databaseConfig.KEY) cfg: ConfigType<...> | 类型安全,推荐 |
configService.get('database.host') | 点路径取,灵活但没类型 |
记忆口诀:
registerAs('名字', factory)= "把配置分到这个名字下面"ConfigType<typeof xxx>= "自动推导这个配置的类型"xxxConfig.KEY= "这个配置在 DI 里的 token"- 核心目的 = 按业务分组 + 类型安全
五、Schema 校验(必填项)
bash
pnpm add joitypescript
import * as Joi from 'joi';
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
JWT_SECRET: Joi.string().min(8).required(),
}),
});启动时缺必填项会直接报错退出,而不是运行期崩。
⚠️ 坑 2:校验失败但没仔细看错误 → 应用启动但
config.get返回 undefined,排查困难。
六、按环境加载文件
typescript
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
`.env.local`,
`.env`,
],
});优先级规则:数组最左边的文件优先级最高(先加载,后加载的同名变量不会覆盖)。
| 数组位置 | 优先级 |
|---|---|
| 第 1 个 | 最高(本地覆盖) |
| 最后 1 个 | 最低(默认兜底) |
示例(NODE_ENV=production,4 个文件都有 DB_HOST):
1. .env.production.local → DB_HOST = 'D' ← 写进去(之前没值)
2. .env.production → 'D' 已存在,跳过
3. .env.local → 'D' 已存在,跳过
4. .env → 'D' 已存在,跳过
最终 = 'D'.local 后缀的文件 NestJS 默认不提交(类似 .gitignore,不用手动配)。
七、在 main.ts 用 config
typescript
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; // 类(给 TS 类型用)
import { AppModule } from './app.module';
async function bootstrap() {
// 1️⃣ 创建 app(NestJS 自动初始化所有 Module,包括 ConfigModule)
const app = await NestFactory.create(AppModule);
// 2️⃣ 从 DI 容器里拿 ConfigService 实例
const cfg = app.get(ConfigService);
// 3️⃣ 读配置
const port = cfg.get<number>('PORT', 3000);
const corsOrigins = cfg.get<string>('CORS_ORIGINS', 'http://localhost:5173').split(',');
// 4️⃣ 全局配置
app.enableCors({ origin: corsOrigins });
app.useGlobalPipes(new ValidationPipe({ transform: true }));
// 5️⃣ 启动
await app.listen(port);
console.log(`App running on http://localhost:${port}`);
}
bootstrap();为什么用 app.get(ConfigService) 而不是 import?
| 方式 | 含义 | 问题 |
|---|---|---|
import { ConfigService } | 拿到类 | 类不能直接调用方法,得 new 一个 |
new ConfigService() | 自己 new | 跳过 NestJS 初始化,env 文件没加载 |
app.get(ConfigService) | 从 DI 容器拿 | ✅ 实例已初始化好,所有依赖齐了 |
核心要点:
- 类可以 import(给 TS 类型用)
- 实例必须
app.get()(从 DI 容器拿) - 不要
new XxxService()(会跳过 NestJS 初始化)
八、TypeORM 配合 ConfigModule
typescript
import { TypeOrmModule } from '@nestjs/typeorm';
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get('DB_HOST'),
port: cfg.get('DB_PORT'),
username: cfg.get('DB_USER'),
password: cfg.get('DB_PASS'),
database: cfg.get('DB_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false, // ⚠️ 生产禁 synchronize
}),
});⚠️ 坑 3:
synchronize: true上生产 → TypeORM 自动改表结构,可能丢数据。
九、运行时改配置(慎用)
typescript
import { ConfigService } from '@nestjs/config';
const cfg = app.get(ConfigService);
cfg.set('FEATURE_FLAG_X', true); // 内存里改只对当前进程生效,多实例部署需走配置中心(Nacos/Apollo)。
十、本章小结
| 要点 | 关键 |
|---|---|
| 核心类 | ConfigService.get<T>(key, default) |
| 文件 | .env 上 gitignore,.env.example 提交 |
| 校验 | validationSchema: Joi.object(...) |
| 命名空间 | registerAs('database', () => ({...})) |
| 配合 ORM | forRootAsync + inject: [ConfigService] |
| 多环境 | envFilePath: ['.env.dev', '.env'] |
动手练习
- 基础加载:写
.env,在 AppService 里读PORT - 命名空间:把 DB 配置抽到
database.config.ts,用registerAs - Joi 校验:加
validationSchema,缺JWT_SECRET时启动失败
下一章:第 13 章:数据库与 TypeORM →