NestJS配置校验Joi学习笔记
NestJS配置校验Joi学习笔记
核心知识点
1. 为什么需要配置校验?
1.1 问题场景
如果没有对 .env 配置进行校验,用户可能传入错误类型或非法值:
env
# 预期:DB_PORT 应为数字 3306
DB_PORT=abc_def # 传入了字符串,数据库连接必然报错1.2 解决思路
| 方案 | 描述 | 优缺点 |
|---|---|---|
| 手动逐项判断 | 读取后用 if/else 校验每个值 | 繁琐、易遗漏、维护成本高 |
| Joi Schema 校验 | 声明式定义校验规则,自动校验 | 简洁、可维护、官方推荐 |
NestJS 官方推荐使用 Joi 库进行配置校验。
2. Joi 库介绍
2.1 安装
bash
pnpm install joi2.2 版本注意事项
如果安装的版本大版本号与课程不一致(如课程为
17.6.0,你安装了18.x或19.x),建议锁定版本安装:
bash
pnpm install joi@172.3 配合 ConfigModule 使用
在 ConfigModule.forRoot() 的 validationSchema 属性中传入 Joi 对象:
typescript
import * as Joi from 'joi';
ConfigModule.forRoot({
validationSchema: Joi.object({
// 在这里定义校验规则...
}),
})3. Joi 常用校验规则
3.1 类型校验
typescript
import * as Joi from 'joi';
Joi.object({
// 数字类型
DB_PORT: Joi.number(),
// 字符串类型
NODE_ENV: Joi.string(),
DB_HOST: Joi.string(),
DB_URL: Joi.string(),
})3.2 默认值(default)
当 .env 文件中未定义该变量时,自动使用默认值:
typescript
Joi.object({
DB_PORT: Joi.number().default(3306),
NODE_ENV: Joi.string().default('development'),
})env
# .env 中未定义 DB_PORT
# → 自动使用默认值 33063.3 枚举校验(valid / allow)
限制值为指定范围内的一项:
typescript
Joi.object({
NODE_ENV: Joi.string().valid('development', 'production'),
// 或使用 allow(别名)
// NODE_ENV: Joi.string().allow('development', 'production'),
})env
NODE_ENV=staging
# 报错:"NODE_ENV" must be one of [development, production]3.4 格式校验
Joi 内置多种格式校验规则:
typescript
Joi.object({
// IP 地址格式
DB_HOST: Joi.string().ip(),
// 域名格式
DB_URL: Joi.string().domain(),
// 邮箱格式
EMAIL: Joi.string().email(),
// URI 格式
API_URL: Joi.string().uri(),
// 日期格式
CREATE_DATE: Joi.string().date(),
})3.5 数值范围校验
typescript
Joi.object({
DB_PORT: Joi.number().min(1024).max(65535),
})3.6 字符串长度校验
typescript
Joi.object({
DB_PASSWORD: Joi.string().min(8).max(32),
API_KEY: Joi.string().length(64),
})3.7 正则校验(pattern)
typescript
Joi.object({
// 自定义正则:只允许字母和数字
USERNAME: Joi.string().pattern(/^[a-zA-Z0-9]+$/),
// API Key 格式:32位十六进制
API_KEY: Joi.string().pattern(/^[0-9a-f]{32}$/),
})3.8 自定义校验(custom)
typescript
Joi.object({
DB_PORT: Joi.number().custom((value, helpers) => {
if (value < 1024 || value > 65535) {
return helpers.error('any.invalid');
}
return value;
}),
})4. 完整校验配置示例
4.1 配置文件
.env(公共配置):
env
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_URL=https://3w.mock.com.env.development(开发环境):
env
DB=mysql_dev.env.production(生产环境):
env
DB=mysql_pro4.2 AppModule 完整配置
typescript
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import { Module } from '@nestjs/common';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.development', '.env'],
validationSchema: Joi.object({
// 数据库类型
DB: Joi.string().valid('mysql_dev', 'mysql_pro'),
// 数据库端口:数字类型,默认 3306
DB_PORT: Joi.number().default(3306).valid(3306, 3307, 3308),
// 运行环境
NODE_ENV: Joi.string()
.valid('development', 'production')
.default('development'),
// 数据库地址:IP 格式
DB_HOST: Joi.string().ip(),
// 数据库 URL:域名格式
DB_URL: Joi.string().domain(),
}),
}),
],
})
export class AppModule {}4.3 校验错误示例
env
# .env.development
DB=mysql_dev
DB_PORT=9999 # 不在 valid 范围 [3306, 3307, 3308] 内
DB_HOST=127.0.0.1AA # 不是合法 IP 地址启动时报错:
code
Error: "DB_PORT" must be one of [3306, 3307, 3308]
Error: "DB_HOST" must be a valid ip address5. load 属性的校验盲区
5.1 问题描述
当使用 load 属性加载公共 .env 文件时,存在一个校验盲区:
typescript
// load 加载的配置可能绕过 validationSchema 校验
ConfigModule.forRoot({
envFilePath: '.env.development',
load: [
() => dotenv.config({ path: '.env' }), // load 加载的 .env 不被校验!
],
validationSchema: Joi.object({
DB_HOST: Joi.string().ip(),
}),
})表现:
.env中的DB_HOST=127.0.0.1AAA(非法 IP).env.development中未定义DB_HOST- 启动不报错 ,但读取到了非法值
原因:validationSchema 只校验通过 envFilePath 指定的文件,load 加载的配置不在校验范围内。
5.2 解决方案:使用数组形式的 envFilePath
envFilePath支持传入字符串数组,数组中靠前的文件优先级更高。
typescript
// 使用数组形式,两个文件都受 validationSchema 校验
ConfigModule.forRoot({
envFilePath: [
'.env.development', // 优先级高:环境专属配置
'.env', // 优先级低:公共共享配置
],
validationSchema: Joi.object({
DB_HOST: Joi.string().ip(),
}),
})覆盖规则(数组从左到右,左边覆盖右边):
code
.env.development > .env
(高优先级) (低优先级)5.3 两种方案对比
| 方案 | 代码 | 校验覆盖 | 推荐度 |
|---|---|---|---|
load + 单文件 envFilePath | envFilePath: '.env.dev' + load: [dotenv] | .env 不被校验 | 不推荐 |
数组 envFilePath | envFilePath: ['.env.dev', '.env'] | 两个文件都被校验 | 推荐 |
6. Joi 常用规则速查表
字符串规则(Joi.string)
| 方法 | 说明 | 示例 |
|---|---|---|
.min(n) | 最小长度 | Joi.string().min(8) |
.max(n) | 最大长度 | Joi.string().max(32) |
.length(n) | 固定长度 | Joi.string().length(64) |
.email() | 邮箱格式 | Joi.string().email() |
.ip() | IP 地址格式 | Joi.string().ip() |
.domain() | 域名格式 | Joi.string().domain() |
.uri() | URI 格式 | Joi.string().uri() |
.hostname() | 主机名格式 | Joi.string().hostname() |
.date() | 日期格式 | Joi.string().date() |
.pattern(regex) | 正则匹配 | Joi.string().pattern(/^[a-z]+$/) |
.valid(...) | 枚举值 | Joi.string().valid('dev', 'prod') |
.default(val) | 默认值 | Joi.string().default('dev') |
.required() | 必填 | Joi.string().required() |
数值规则(Joi.number)
| 方法 | 说明 | 示例 |
|---|---|---|
.min(n) | 最小值 | Joi.number().min(0) |
.max(n) | 最大值 | Joi.number().max(65535) |
.integer() | 整数 | Joi.number().integer() |
.positive() | 正数 | Joi.number().positive() |
.port() | 端口号(1-65535) | Joi.number().port() |
.valid(...) | 枚举值 | Joi.number().valid(3306, 3307) |
.default(val) | 默认值 | Joi.number().default(3306) |
通用规则
| 方法 | 说明 | 适用类型 |
|---|---|---|
.valid(...) | 限定可选值 | string / number |
.default(val) | 默认值 | 所有类型 |
.required() | 必填 | 所有类型 |
.optional() | 可选 | 所有类型 |
.custom(fn) | 自定义校验 | 所有类型 |
代码实战案例
需求描述
构建一个带完整 Joi 校验的 NestJS 配置系统,涵盖类型、格式、范围、枚举等多种校验规则。
完整实现
第一步:安装依赖
bash
pnpm install @nestjs/config joi cross-env -D第二步:创建配置文件
.env:
env
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=123456
DB_URL=https://api.mock.com.env.development:
env
DB=mysql_dev.env.production:
env
DB=mysql_pro第三步:配置 AppModule
typescript
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import { Module } from '@nestjs/common';
import * as Joi from 'joi';
import { UserModule } from './user/user.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
// 数组形式:.env.development 优先级高于 .env
envFilePath: [`.env.${process.env.NODE_ENV || 'development'}`, '.env'],
// Joi 校验规则
validationSchema: Joi.object({
// 数据库类型:限定可选值
DB: Joi.string().valid('mysql_dev', 'mysql_pro'),
// 数据库端口:数字 + 默认值 + 枚举范围
DB_PORT: Joi.number().default(3306).valid(3306, 3307, 3308),
// 运行环境:字符串 + 枚举 + 默认值
NODE_ENV: Joi.string()
.valid('development', 'production')
.default('development'),
// 数据库地址:IP 格式
DB_HOST: Joi.string().ip(),
// 数据库 URL:域名格式
DB_URL: Joi.string().domain(),
}),
}),
UserModule,
],
})
export class AppModule {}第四步:在 Controller 中使用
typescript
// user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConfigEnum } from '../enum/config.enum';
@Controller('user')
export class UserController {
constructor(private configService: ConfigService) {}
@Get()
getUsers() {
return {
db: this.configService.get(ConfigEnum.DB),
host: this.configService.get(ConfigEnum.DB_HOST),
port: this.configService.get<number>(ConfigEnum.DB_PORT),
url: this.configService.get(ConfigEnum.DB_URL),
};
}
}第五步:配置运行脚本
json
{
"scripts": {
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:pro": "cross-env NODE_ENV=production node dist/main"
}
}测试验证
bash
# 正常启动
pnpm start:dev
# 在 .env 中将 DB_HOST 改为 127.0.0.1AAA 后启动
# Error: "DB_HOST" must be a valid ip address
# 在 .env 中将 DB_PORT 改为 9999 后启动
# Error: "DB_PORT" must be one of [3306, 3307, 3308]
# 将 NODE_ENV 改为 staging 后启动
# Error: "NODE_ENV" must be one of [development, production]常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
"DB_PORT" must be a number | .env 中 DB_PORT 值为非数字字符串 | 确保值是纯数字,如 3306 |
load 加载的配置没被校验 | validationSchema 不覆盖 load 加载的文件 | 改用数组形式的 envFilePath |
| Joi 版本不兼容 | 安装了大版本不同的 Joi | pnpm install joi@17 锁定版本 |
default 不生效 | 变量已被定义为空字符串 DB_PORT= | .env 中删除该行或注释掉 |
| 找不到 Joi 校验规则 | 不知道有哪些规则可用 | 查看 Joi 官方 API 文档 |
学习要点总结
- Joi 是声明式校验库:通过链式调用定义规则,简洁高效,NestJS 官方推荐
validationSchema:在ConfigModule.forRoot()中配置,应用启动时自动校验default():未定义的变量自动使用默认值,减少必填配置项valid()/allow():枚举校验,限制值在指定范围内- **
load属性的校验盲区**:load加载的配置不受validationSchema校验,应改用数组形式的envFilePath
延伸学习资源
官方文档
后续课程预告
- 命名空间配置:使用
forFeature()按模块拆分配置 - 自定义校验:结合业务逻辑实现更复杂的校验规则
最佳实践
code
配置校验三件套:
1. 类型校验(number / string)→ 防止类型错误
2. 格式校验(ip / domain / email)→ 防止格式错误
3. 枚举/范围校验(valid / min / max)→ 防止值越界