- Published on
zod 的使用 | validate
- Authors
- Name
- Shelton Ma
1. zod vs express-validate
- express-validator 适用场景
- Express 项目中,快速添加参数校验.
- 需处理简单的 req.body、req.query、req.params 验证.
- 需要结合 Express 中间件流畅处理错误的场景.
- 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
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(), });
验证中间件
// 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 }); } };
路由
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
来验证请求数据.
使用
.extend()
const TicketUpdateSchema = TicketSchema.extend({ _id: z.string(), // 这里去掉 optional,使其变为必填 });
使用
.merge()
const TicketUpdateSchema = TicketSchema.merge( z.object({ _id: z.string(), // 这里覆盖原来的可选字段,使其必填 }) );
使用
.omit()
const TicketIdSchema = TicketSchema.extend({ _id: z.string().nonempty(), }).omit({ content: true, status: true, handler: true });
req.body
, 同时扩展 Request 的类型来让 req.body
具有正确的类型
4. 验证中间件可以直接解析 利用 TypeScript 的**泛型(Generics)**特性扩展
Express
的Request
接口// 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; }
清理参数中的空值
// 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>; };
中间件使用
// 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(); } };
请求直接处理
// 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>;