Skip to content
第 12 章 后端 ⏱ 12 分钟阅读

第 12 章:配置管理 ​

学习目标 ​

  • 用 @nestjs/config 管理配置
  • 区分环境变量与配置文件
  • 校验配置项
  • 避开 3 个配置坑

一、为什么需要 ConfigModule ​

直接 process.env.DB_URL 散落各处,难统一、难校验。@nestjs/config 提供:

  • 集中加载 .env
  • 类型安全
  • Schema 校验
  • 命名空间(分模块配置)

二、安装与基础用法 ​

bash
pnpm add @nestjs/config
typescript
// 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.KEYDI 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 joi
typescript
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', () => ({...}))
配合 ORMforRootAsync + inject: [ConfigService]
多环境envFilePath: ['.env.dev', '.env']

动手练习 ​

  1. 基础加载:写 .env,在 AppService 里读 PORT
  2. 命名空间:把 DB 配置抽到 database.config.ts,用 registerAs
  3. Joi 校验:加 validationSchema,缺 JWT_SECRET 时启动失败

下一章:第 13 章:数据库与 TypeORM →

本站基于 VitePress 构建 · 由 StackHub 团队维护