Published on

zod 的使用 | validate

Authors
  • avatar
    Name
    Shelton Ma
    Twitter

1. zod vs express-validate

  1. express-validator 适用场景
    • Express 项目中,快速添加参数校验.
    • 需处理简单的 req.body、req.query、req.params 验证.
    • 需要结合 Express 中间件流畅处理错误的场景.
  2. zod 适用场景
    • TypeScript 项目,追求强类型和类型推断的最佳实践.
    • 复杂数据结构、嵌套对象验证(如嵌套数组、对象).
    • 需要独立于 Express、适用于其他框架或纯函数逻辑.

2. 目录结构

# 避免和prisma的文件夹冲突, 设置 zodSchema/validateSchema
src/
├─ app.ts                   # 应用入口
├─ server.ts                # 服务启动(监听端口)
├─ config/                  # 配置文件 (环境变量封装、常量)
│    └─ index.ts
├─ routes/                  # 路由定义
│    └─ user.route.ts
│    └─ auth.route.ts
├─ controllers/             # 控制器层
│    └─ user.controller.ts
│    └─ auth.controller.ts
├─ services/                # 服务层(业务逻辑)
│    └─ user.service.ts
│    └─ auth.service.ts
├─ validators/              # 请求校验 (可用 zod / joi / class-validator)
│    └─ user.validator.ts
├─ models/                  # 数据库模型
│    └─ user.model.ts
├─ utils/                   # 工具函数
│    └─ logger.ts
└─ middlewares/             # 中间件
     └─ error.middleware.ts
     └─ auth.middleware.ts
  1. Zod Schema 定义

    // zodSchema/userSchema.ts
    import { z } from 'zod';
    
    export const userSchema = z.object({
      username: z.string().min(3, '用户名至少3个字符'),
      email: z.string().email('邮箱格式不正确'),
      age: z.number().int().min(18, '年龄必须是大于18的整数').optional(),
    });
    
  2. 验证中间件

    // controllers/userController.ts
    import { NextFunction, Request, Response } from 'express';
    import { userSchema } from '../validateSchema/userSchema';
    
    export const validateUser = (req: Request, res: Response, next: NextFunction) => {
      try {
        req.body = userSchema.parse(req.body);
        next();
      } catch (error) {
        res.status(400).json({ errors: error.errors });
      }
    };
    
  3. 路由

    import express from 'express';
    import { createUser } from '../controllers/userController';
    import { validateUser } from '../middlewares/validateUser';
    
    const router = express.Router();
    
    router.post('/user', validateUser, createUser);
    
    export default router;
    

3. 最佳实践

1. 错误处理

try {
  ...
}catch(error){
  if (error instanceof ZodError) {
    return res.status(400).json({
      success: false,
      errors: error.errors.map(err => ({
        field: err.path.join('.'),
        message: err.message
      }))
    });
  }

  // 非 Zod 错误的处理
  res.status(500).json({
    success: false,
    message: 'Internal Server Error'
  });
}

2. 枚举类型复用

import { z } from 'zod';

// 定义 TicketStatus 类型和常量,确保类型和数据一致
const TicketStatusEnum = ["pending", "resolved", "ignored"] as const;

// 用于Model使用
type TicketStatus = (typeof TicketStatusEnum)[number]; // ✅ 复用枚举值生成类型

// 创建 Zod Schema
export const ticketSchema = z.object({
  title: z.string().min(3, "标题至少3个字符"),
  description: z.string().optional(),
  status: z.enum(TicketStatusEnum), // ✅ 复用枚举常量
});

3. zod的扩展和调整

可以使用 .extend().merge() 来基于 TicketSchema 创建一个新的模式, 然后在 PUT 路由中使用 TicketUpdateSchema 来验证请求数据.

  1. 使用 .extend()

    const TicketUpdateSchema = TicketSchema.extend({
      _id: z.string(), // 这里去掉 optional,使其变为必填
    });
    
  2. 使用 .merge()

    const TicketUpdateSchema = TicketSchema.merge(
      z.object({
        _id: z.string(), // 这里覆盖原来的可选字段,使其必填
      })
    );
    
  3. 使用 .omit()

    const TicketIdSchema = TicketSchema.extend({
      _id: z.string().nonempty(),
    }).omit({ content: true, status: true, handler: true });
    

4. 验证中间件可以直接解析 req.body, 同时扩展 Request 的类型来让 req.body 具有正确的类型

  1. 利用 TypeScript 的**泛型(Generics)**特性扩展 ExpressRequest 接口

    // src/types/request.ts
    import { Request } from "express";
    import { ParamsDictionary, Query } from "express-serve-static-core";
    
    export interface TypedRequest<T> extends Request {
      body: T;
    }
    
    export interface TypedRequest<
      TBody = unknown,
      TQuery extends Query = Query,
      TParams extends ParamsDictionary = ParamsDictionary
    > extends Request {
      body: TBody;
      query: TQuery;
      params: TParams;
    }
    
  2. 清理参数中的空值

    // src/validators/index.ts
    // 通用 empty/null -> undefined 处理
    export const emptyToUndefined = (val: unknown) =>
      val === "" || val === null ? undefined : val;
    
    // 自动清理对象里 undefined 字段
    export const cleanObject = <T extends Record<string, any>>(
      obj: T
    ): Partial<T> => {
      return Object.fromEntries(
        Object.entries(obj).filter(([_, v]) => v !== undefined)
      ) as Partial<T>;
    };
    
  3. 中间件使用

    // middlewares/validateRequest.ts
    import { ZodError, ZodSchema } from "zod";
    import { Request, Response, NextFunction } from "express";
    import { cleanObject } from "../../../validators";
    
    export const validateRequest =
      (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
        const parseResult = schema.safeParse({
          body: req.body,
          query: req.query,
          params: req.params,
        });
        if (!parseResult.success) {
          throw new ZodError(parseResult.error.errors);
        } else {
          const { body, query, params } = parseResult.data;
    
          if (body) req.body = cleanObject(body);
          if (query) req.query = cleanObject(query);
          if (params) req.params = cleanObject(params);
          next();
        }
      };
    
  4. 请求直接处理

    // xx/routes/user.ts
    app.put("/user", validateRequest(z.object({ body: UserUpdateSchema })),, async (req: TypedRequest<UserUpdateType>, res) => {
      const userData = req.body; // req.body 现在有正确的类型
      res.json({ message: "user updated successfully", data: userData });
    });
    app.put("/user", validateRequest(z.object({ query: UserQuerySchema })),, async (req: TypedRequest<unknown, UserQueryType>, res) => {
      const userQueryData = req.query; // req.query 现在有正确的类型
      res.json({ message: "user updated successfully", data: userQueryData });
    });
    

5. 在创建schema时同时生成推断类型

import { z } from "zod";

// Zod schema 定义
export const TicketUpdateSchema = z.object({
  _id: z.string().optional(),
  content: z.string().nonempty(),
  status: z.enum(["open", "closed"]).optional(),
  handler: z.string().optional(),
  creator: z.string().optional(),
  securityEvent: z.string().optional(),
  securityAlert: z.string().optional(),
  attachments: z.array(z.object({ url: z.string() })).optional(),
});

// 从 schema 推导出的类型
export type TicketUpdateType = z.infer<typeof TicketUpdateSchema>;