Nodejs 使用 class-validator 替换 Joi 包的方法

Nodejs 使用 class-validator 替换 Joi 包的方法

前言

对每个接口的传入参数进行校验,是一个 Web 后端项目的必备功能,有一个 npm 包叫Joi可以很优雅的完成这个工作,比如这样子:

const schema = {
    userId: Joi.string()
};
const {error, value} = Joi.validate({ userId: 'a string' }, schema);

我们使用 Typescript 是希望得到明确的类型定义,减少出错的可能性。在一个后端项目中,给每个接口定义它的传入参数结构以及返回结果的结构,是一件很值得做的事情,因为这样给后续的维护带来极大的便利。比如这样子:

export type IFooParam = {
  userId: string
}

export type IFooResponse = { name: string }

async foo (param: IFooParam): Promise<IFooResponse> { // Your business code return {name: ‘bar’} }

现在问题就来了,如果传入参数希望加多一个字段,我们必须得修改 2 个地方,一个是 Joi 的校验,一个是 IFooParam 类型的定义。有没有好的办法解决这个问题呢?

Class-validaotr

有一个 npm 包叫class-validator, 是采用注解的方式进行校验,底层使用的是老牌的校验包validator.js
这次试用,发现通过一些小包装,居然做到像 Joi 一样优雅的写法,而且更好用!

定义传入 /返回结构

import {Length, Min, Max} from 'class-validator'

export class IRegister { @Length(11) phone: string

@Length(2, 10) name: string

@Min(18) @Max(50) age: number }

class Button { text: string }

export class ORegister { /**

  • user’s id */ userId: string

buttons: Button[] }

这里定义了 2 个类,IRegister 为传入参数,通过 class-validator 规定的注解方式做校验,ORegister 为返回结果。

class-validator 官方提供的方式还不能直接对一个请求的 body 进行校验,它要求必须要是 IRegister 类的一个对象,所以需要做一些处理。

使用 class-transformer 做转化

跟 class-validator 的作者也开源了另外一个包,叫class-transformer, 可以将一个 json 转成指定的类的对象,官方的例子是这样的:

import {plainToClass} from "class-transformer";

let users = plainToClass(User, userJson); // to convert user plain object a single user. also supports arrays

利用这一点,我们写一个小工具:

import * as classTransformer from 'class-transformer'
import {validate} from 'class-validator'
import * as lodash from 'lodash'

export class ValidateUtil { private static instance: ValidateUtil

private constructor () { }

static getInstance () { return this.instance || (this.instance = new ValidateUtil()) }

async validate (Clazz, data): Promise<any> { const obj = classTransformer.plainToClass(Clazz, data) const errors = await validate(obj) if (errors.length > 0) { console.info(errors) throw new Error(lodash.values(errors[0].constraints)[0]) } return obj } }

这个小工具提供了一个 validate 方法,第一个参数是一个类定义,第二个是一个 json,它先利用 class-transformer 将 json 转成指定类的对象,然后使用 class-validator 做校验,如果校验错误将抛出错误,否则返回转化后的对象。

在 Controller 中使用

有了上面的工具,就可以方便地在代码中对传入参数做校验了,比如这样:

  static async register(ctx) {
    const iRegister = await ValidateUtil.getInstance().validate(IRegister, ctx.request.body)
    const oRegister = await UserService.register(iRegister)
    ctx.body = oRegister
  }

新问题

到了这里,完美地使用 class-validator 替换掉了 Joi。

但是还有一个问题没解决,也是之前一直遗留的问题。

我们使用apidoc编写接口文档,当新增或修改一个接口时,是通过编写一段注释,让 apidoc 自动生成 html 文档,将文档地址发给前端,可以减少双方的频繁沟通,而且对前端的体验也是非常好的。比如写这样一段注释:

  /**
   * [@api](/user/api) {post} /user/registerOld registerOld
   * [@apiGroup](/user/apiGroup) user
   * [@apiName](/user/apiName) registerOld
   * [@apiParam](/user/apiParam) {String} name user's name
   * [@apiParam](/user/apiParam) {Number} age user's age
   * [@apiSuccess](/user/apiSuccess) {String} userId user's id 
   */
  router.post('/user/registerOld', UserController.register)

apidoc 会帮我们生成这样的文档: oldApidocDemo

问题比较明显,当我们要新增一个参数时,需要修改一次类的定义,同时还要修改一次 apidoc 的注释,很烦,由于很烦,文档会慢慢变得没人维护,新同事就会吐槽没有文档或者文档太旧了。

理想的情况是代码即文档,只需要修改类的定义,apidoc 文档自动更新。

探索 apidoc 根据 class-validator 的定义生成

从同事的分享中得知一个废弃的 npm 包,叫apidoc-plugin-ts, 可以实现根据 ts 的 interface 定义来生成 apidoc 的。官方的例子:

filename: ./employers.ts

export interface Employer { /**

  • Employer job title / jobTitle: string; /*
  • Employer personal details */ personalDetails: { name: string; age: number; } } @apiInterface (./employers.ts) {Person}

会转化成:

 [@apiSuccess](/user/apiSuccess) {String} jobTitle Job title
 [@apiSuccess](/user/apiSuccess) {Object} personalDetails Empoyer personal details
 [@apiSuccess](/user/apiSuccess) {String} personalDetails.name
 [@apiSuccess](/user/apiSuccess) {Number} personalDetails.age

虽然不知道为什么作者要废弃它,但是它的思想很好,源码也很有帮助。

给我的启发是,参考这个 npm 包,写一个针对 class 定义来生成 apidoc 的插件就行了。

造轮子: apidoc-plugin-class-validator

轮子的制造细节不适合在这里陈述,基本上参考 apidoc-plugin-ts,目前已经发布在 npm 上了,apidoc-plugin-class-validator

使用 apidoc-plugin-class-validator

以上面的注册接口为例,使用方法:

  /**
   * [@api](/user/api) {post} /user/register register
   * [@apiGroup](/user/apiGroup) user
   * [@apiName](/user/apiName) register
   * [@apiParamClass](/user/apiParamClass) (src/user/io/Register.ts) {IRegister}
   * [@apiSuccessClass](/user/apiSuccessClass) (src/user/io/Register.ts) {ORegister}
   */
  router.post('/user/register', UserController.register)

就会生成文档: demo

后续新增字段,只需修改 IRegister 类的定义就行,真正做到了修改一处,处处生效,代码即文档的效果。

本文的 demo 代码在这里,这是一个简单的 web 后端项目,看代码更容易理解。


3 回复

看来大家都有这个痛点,之前我们用的是 https://github.com/SijieCai/ts-class-validator


都用过. joi 也能很好的推导出 typing 的. 但是稍微复杂一点点的还是得上 joi. 例如 Joi.when. 验证需求不复杂的话, class-validator 还是看的更顺眼

在Node.js中,从Joi包迁移到class-validator进行验证,可以显著提升代码的可读性和类型安全性,特别是当你使用TypeScript时。下面是一个简单的示例,展示了如何使用class-validator替换Joi进行对象验证。

使用Joi进行验证

const Joi = require('joi');

const schema = Joi.object({
  username: Joi.string().required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(120)
});

const validate = async (data) => {
  try {
    await schema.validateAsync(data);
    console.log('Validation successful!');
  } catch (error) {
    console.error('Validation failed:', error.details);
  }
};

validate({ username: 'test', email: 'invalid-email', age: -5 });

使用class-validator进行验证

首先,安装class-validator和class-transformer(用于对象转换):

npm install class-validator class-transformer

然后,定义验证类和验证逻辑:

import { validate, ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';

class UserDto {
  @IsString()
  @IsNotEmpty()
  username!: string;

  @IsEmail()
  @IsNotEmpty()
  email!: string;

  @IsInt()
  @Min(0)
  @Max(120)
  age!: number;
}

async function validateUser(user: UserDto) {
  const errors = await validate(user);
  if (errors.length > 0) {
    console.error('Validation failed:', errors);
  } else {
    console.log('Validation successful!');
  }
}

validateUser({ username: 'test', email: 'invalid-email', age: -5 });

以上代码展示了如何从Joi迁移到class-validator,利用装饰器实现更加简洁和类型安全的验证逻辑。

回到顶部