CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
NestJS 实战入门/课时/ConfigModule
RawBlame
课时目录12 节

NestJS 实战入门

  1. 01NestJS 是什么
  2. 02项目创建与结构
  3. 03Module 与依赖注入
  4. 04Controller 与路由
  5. 05Service 层
  6. 06Pipe 与校验
  7. 07Guard 与认证
  8. 08ConfigModule
  9. 09数据库接入入门
  10. 10异常与过滤器
  11. 11测试
  12. 12综合练习

ConfigModule

环境变量、.env、配置注入。

25 分钟

ConfigModule

这一课你将能做到

  1. 安装并在根模块注册 ConfigModule.forRoot({ isGlobal: true })
  2. 用 ConfigService 读取端口、JWT 密钥等配置,替换散落各处的 process.env
  3. 准备 .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 极早期逻辑
注入 ConfigServiceNest 应用内的默认做法
配置命名空间 + 校验中大型项目,需要类型化和 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

确认 .gitignore 中包含:

.env
.env.local

新人拉代码后只需 cp .env.example .env 再填值,就知道项目需要哪些配置项——example 文件是团队的配置契约。

步骤 3:用 ConfigService 读取端口

修改 src/main.ts:

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 比运行到鉴权才报错友好得多,也避免带着错误配置对外服务。

讲清楚要点

  1. 配置随环境变化,绝不进源码:密钥、主机名、端口一律外部化。
  2. 应用内只认 ConfigService:散落的 process.env 会让测试和重构都变困难。
  3. isGlobal 减少样板:根模块 forRoot 一次,所有业务模块直接注入。
  4. example 是团队契约:提交 .env.example、忽略真实 .env,新人按模板配置即可。
  5. 环境区分靠注入而不是 if 分支:开发、测试、生产各自提供环境变量,同一份代码不改一行。
  6. 关键配置必须校验:启动时失败成本最低,越晚发现代价越大。

你可能会懵的地方

  • .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 兜底生产弱密钥被带进生产生产缺密钥应直接拒绝启动

卡点急救

  1. ConfigService 注入报错:确认根模块已 ConfigModule.forRoot 且 isGlobal: true;使用命名空间时记得在 load 数组中注册。
  2. 生产读取不到 .env:这是正常现象,生产通常没有该文件,确认平台已注入对应环境变量。
  3. Joi 校验导入失败:确认已安装 joi;不想引依赖时可以用自定义校验函数在 main.ts 启动前手动检查必填项。

面试常问

  • 为什么要做配置外部化? 同一套代码要在开发、测试、生产多环境运行,配置必然不同;外部化后环境各自注入,密钥不进仓库,也符合十二要素应用(12-Factor App)的配置原则。

  • NestJS 读取配置的标准方式是什么? 通过 @nestjs/config 的 ConfigModule 加载 .env 与环境变量,业务类注入 ConfigService;需要类型化时用 registerAs 定义命名空间配置。

  • 如何保证应用不会带着错误配置启动? 用 validationSchema(Joi)或自定义校验在启动时检查必填项与格式,fail fast;这比运行期才发现配置缺失更安全。

自测三问

  1. ConfigModule.forRoot({ isGlobal: true }) 解决了什么重复劳动?
  2. .env 和 .env.example 哪个该进 Git、哪个不该?
  3. 为什么测试中更推荐注入假的 ConfigService,而不是依赖真实环境变量?

小结一句话

ConfigModule 提供全局可注入的环境配置,密钥不进仓库,关键配置在启动时校验。

延伸

  • Configuration(官方配置文档)
  • 12-Factor App 配置原则
  • 下一课预告:数据库接入入门,对照 TypeORM 与 Prisma 理解 Entity、Repository,但本课程不会真的同步数据库。
加载本节练习…
← 上一课Guard 与认证下一课 →数据库接入入门