{T}

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 joi

2.2 版本注意事项

如果安装的版本大版本号与课程不一致(如课程为 17.6.0,你安装了 18.x19.x),建议锁定版本安装:

bash
pnpm install joi@17

2.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
# → 自动使用默认值 3306

3.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_pro

4.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 address

5. 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 + 单文件 envFilePathenvFilePath: '.env.dev' + load: [dotenv].env 不被校验不推荐
数组 envFilePathenvFilePath: ['.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.envDB_PORT 值为非数字字符串确保值是纯数字,如 3306
load 加载的配置没被校验validationSchema 不覆盖 load 加载的文件改用数组形式的 envFilePath
Joi 版本不兼容安装了大版本不同的 Joipnpm install joi@17 锁定版本
default 不生效变量已被定义为空字符串 DB_PORT=.env 中删除该行或注释掉
找不到 Joi 校验规则不知道有哪些规则可用查看 Joi 官方 API 文档

学习要点总结

  1. Joi 是声明式校验库:通过链式调用定义规则,简洁高效,NestJS 官方推荐
  2. validationSchema:在 ConfigModule.forRoot() 中配置,应用启动时自动校验
  3. default():未定义的变量自动使用默认值,减少必填配置项
  4. valid() / allow():枚举校验,限制值在指定范围内
  5. ** load 属性的校验盲区**:load 加载的配置不受 validationSchema 校验,应改用数组形式的 envFilePath

延伸学习资源

官方文档

后续课程预告

  • 命名空间配置:使用 forFeature() 按模块拆分配置
  • 自定义校验:结合业务逻辑实现更复杂的校验规则

最佳实践

code
配置校验三件套:
1. 类型校验(number / string)→ 防止类型错误
2. 格式校验(ip / domain / email)→ 防止格式错误
3. 枚举/范围校验(valid / min / max)→ 防止值越界