ConfigModule
环境变量、.env、配置注入。
ConfigModule
这一课你将能做到
- 安装并在根模块注册
ConfigModule.forRoot({ isGlobal: true })
- 用
ConfigService 读取端口、JWT 密钥等配置,替换散落各处的 process.env
- 准备
.env.example 模板,并理解「缺少关键配置就让应用启动失败」的 fail-fast 思路
为什么要学
把端口、密钥、上游服务地址写死在源码里,必然出现「本机能跑、服务器一启动就炸」。而且密钥进了 Git 历史,即使后来删掉也已经泄露。
@nestjs/config 的解法是:启动时从 .env 文件和系统环境变量加载配置,统一放进可注入的 ConfigService。业务代码只依赖注入的配置,本地开发靠 .env,测试里可以注入假值,生产环境直接由容器或云平台注入环境变量,一份代码到处运行。
它对应 Spring Boot 的 application.yml 加 @Value / @ConfigurationProperties,以及用 profile 区分 dev、prod 的做法。
先建立图像
.env 文件 / 系统环境变量
↓
ConfigModule.forRoot(启动时加载一次)
↓
ConfigService.get<number>("PORT")
↓
main.ts 监听端口 / AuthModule 读 JWT 密钥 / Service 读上游地址
进 Git:.env.example(占位,无密钥)
不进 Git:.env、.env.local(真实值)
| 读取方式 | 适用场景 |
|---|
直接 process.env.X | 一次性脚本、main.ts 极早期逻辑 |
注入 ConfigService | Nest 应用内的默认做法 |
| 配置命名空间 + 校验 | 中大型项目,需要类型化和 fail-fast |
跟着做
步骤 1:安装并注册 ConfigModule
npm install @nestjs/config
修改 src/app.module.ts:
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AuthModule } from "./auth/auth.module";
import { QuestionsModule } from "./questions/questions.module";
import { SubmissionsModule } from "./submissions/submissions.module";
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 别的模块无需重复 imports
envFilePath: [".env.local", ".env"], // 靠前的文件优先级更高
}),
AuthModule,
QuestionsModule,
SubmissionsModule,
],
})
export class AppModule {}
应用应照常启动,暂时还没有地方读取配置。
步骤 2:写 .env 与 .env.example
项目根目录创建 .env(真实值,不提交):
PORT=3000
JWT_SECRET=dev-only-secret-please-change
JWT_EXPIRES_IN=2h
QUESTIONS_API_BASE=http://localhost:8080
创建 .env.example(占位模板,可以提交):
PORT=3000
JWT_SECRET=
JWT_EXPIRES_IN=2h
QUESTIONS_API_BASE=http://localhost:8080
新人拉代码后只需 cp .env.example .env 再填值,就知道项目需要哪些配置项——example 文件是团队的配置契约。
步骤 3:用 ConfigService 读取端口
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = app.get(ConfigService);
app.setGlobalPrefix("api");
const port = config.get<number>("PORT", 3000);
await app.listen(port);
}
bootstrap();
把 .env 中 PORT 改为 3001,重启后服务会监听 3001。注意:环境变量在进程启动时读取,修改 .env 需要手动重启,热重载不保证重新加载。
步骤 4:JWT 密钥改用配置(异步注册)
JwtModule 需要在模块初始化阶段拿到密钥,使用 registerAsync:
import { ConfigModule, ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>("JWT_SECRET", "insecure-default"),
signOptions: { expiresIn: config.get<string>("JWT_EXPIRES_IN", "2h") },
}),
}),
],
// controllers / providers / exports 保持不变
})
export class AuthModule {}
由于 ConfigModule 是全局模块,imports: [ConfigModule] 也可省略,但显式写出来可读性更好。
步骤 5:在业务类中注入配置
例如刷题平台要调用本站 Java 题库服务做同步,Service 这样写:
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class QuestionSyncService {
constructor(private readonly config: ConfigService) {}
baseUrl(): string {
return this.config.get<string>(
"QUESTIONS_API_BASE",
"http://localhost:8080",
);
}
}
单元测试时不需要真实 .env,直接提供假的 ConfigService:
const moduleRef = await Test.createTestingModule({
providers: [
QuestionSyncService,
{ provide: ConfigService, useValue: { get: () => "http://test-host" } },
],
}).compile();
步骤 6:类型化配置命名空间(推荐进阶写法)
新建 src/config/jwt.config.ts:
import { registerAs } from "@nestjs/config";
export default registerAs("jwt", () => ({
secret: process.env.JWT_SECRET ?? "insecure-default",
expiresIn: process.env.JWT_EXPIRES_IN ?? "2h",
}));
在 AppModule 中加入 load: [jwtConfig],使用处注入:
import { Injectable } from "@nestjs/common";
import { ConfigType } from "@nestjs/config";
import jwtConfig from "../config/jwt.config";
@Injectable()
export class SomeService {
constructor(
@Inject(jwtConfig.KEY)
private readonly jwt: ConfigType<typeof jwtConfig>,
) {}
info() {
return `${this.jwt.secret.slice(0, 3)}*** / ${this.jwt.expiresIn}`;
}
}
好处是配置有分组、字段有类型,this.jwt.secret 一定是 string,不用每次手动传泛型。
步骤 7:(可选)启动校验,缺配置直接拒绝启动
@nestjs/config 支持传入 Joi schema(需要时 npm install joi):
import * as Joi from "joi";
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
PORT: Joi.number().default(3000),
JWT_SECRET: Joi.string().min(16).required(),
JWT_EXPIRES_IN: Joi.string().default("2h"),
}),
validationOptions: { abortEarly: false },
});
JWT_SECRET 缺失或太短时,应用在启动阶段就失败并打印全部问题。fail fast 比运行到鉴权才报错友好得多,也避免带着错误配置对外服务。
讲清楚要点
- 配置随环境变化,绝不进源码:密钥、主机名、端口一律外部化。
- 应用内只认 ConfigService:散落的
process.env 会让测试和重构都变困难。
- isGlobal 减少样板:根模块 forRoot 一次,所有业务模块直接注入。
- example 是团队契约:提交
.env.example、忽略真实 .env,新人按模板配置即可。
- 环境区分靠注入而不是 if 分支:开发、测试、生产各自提供环境变量,同一份代码不改一行。
- 关键配置必须校验:启动时失败成本最低,越晚发现代价越大。
你可能会懵的地方
-
.env 改了为什么不生效?
环境变量在进程启动时读取,必须重启;还要检查是否存在同名系统环境变量覆盖了文件值(系统变量优先级更高)。
-
生产环境也用 .env 文件吗?
容器、Kubernetes、云平台一般直接注入环境变量,ConfigService 同样能读到;.env 文件主要服务于本地开发。
-
ConfigService.get 拿到的是字符串还是数字?
环境变量本质都是字符串。PORT 若要当数字使用,需显式 Number(...),或用命名空间配置在工厂里完成转换。
-
.env.example 里的密钥留空,本地怎么跑?
example 只是清单;复制为 .env 后填入本地开发值。开发密钥可以简化,但绝不应该用同一个密钥去签生产 Token。
-
多个模块各自 forRoot 会怎样?
配置会重复加载、优先级混乱。初学者默认在根模块注册一次并设为全局即可。
错法对照
| 错法 | 问题 | 正解 |
|---|
真实密钥写进源码或 .env 被提交 | 密钥泄露,且永久留在 Git 历史 | ignore + example + 环境注入 |
业务代码里写死 http://localhost:8080 | 换环境必须改代码 | ConfigService 读取 |
| 每个模块重复 ConfigModule.forRoot | 行为不一致 | 根模块全局一次 |
| 缺少密钥仍正常启动 | 运行到鉴权才暴露问题 | validationSchema fail-fast |
用 ?? "默认密钥" 给 JWT_SECRET 兜底生产 | 弱密钥被带进生产 | 生产缺密钥应直接拒绝启动 |
卡点急救
- ConfigService 注入报错:确认根模块已
ConfigModule.forRoot 且 isGlobal: true;使用命名空间时记得在 load 数组中注册。
- 生产读取不到 .env:这是正常现象,生产通常没有该文件,确认平台已注入对应环境变量。
- Joi 校验导入失败:确认已安装 joi;不想引依赖时可以用自定义校验函数在 main.ts 启动前手动检查必填项。
面试常问
-
为什么要做配置外部化?
同一套代码要在开发、测试、生产多环境运行,配置必然不同;外部化后环境各自注入,密钥不进仓库,也符合十二要素应用(12-Factor App)的配置原则。
-
NestJS 读取配置的标准方式是什么?
通过 @nestjs/config 的 ConfigModule 加载 .env 与环境变量,业务类注入 ConfigService;需要类型化时用 registerAs 定义命名空间配置。
-
如何保证应用不会带着错误配置启动?
用 validationSchema(Joi)或自定义校验在启动时检查必填项与格式,fail fast;这比运行期才发现配置缺失更安全。
自测三问
ConfigModule.forRoot({ isGlobal: true }) 解决了什么重复劳动?
.env 和 .env.example 哪个该进 Git、哪个不该?
- 为什么测试中更推荐注入假的 ConfigService,而不是依赖真实环境变量?
小结一句话
ConfigModule 提供全局可注入的环境配置,密钥不进仓库,关键配置在启动时校验。
延伸