停止在 Next.js 中使用 Zod 复制验证逻辑
Source: Dev.to
请提供您希望翻译的具体文本内容,我将为您翻译成简体中文。
如今,我们不再只是前端开发者
使用 Next.js,我们在同一个项目中同时构建客户端和服务器端代码,创建全栈应用而无需切换上下文。这也意味着我们需要在多个地方进行验证。
- 我们在客户端进行验证,以提升用户体验。
- 我们在服务器端再次验证,以确保安全。
问题在于,这通常会导致逻辑重复:相同的规则会被写两遍——一次用于表单,另一次用于 API。当规则发生变化时,你必须记得在所有地方都更新它,导致不必要的同步麻烦。
如果一个单一的模式能够同时在客户端和服务器端进行验证会怎样?
引入 Zod。Zod‑first 的方法让这成为可能:唯一的真相来源,随处验证。让我们看看它是如何工作的。
场景
想象一个典型的注册表单。你需要三个字段:name、email 和 website。你的第一步可能是创建一个 TypeScript 接口:
// lib/types/user.ts
export interface SignupInput {
name: string;
email: string;
website: string;
}
在编辑器里看起来很整洁,但接口在运行时并不会进行任何验证——它们在代码编译成 JavaScript 时会消失。因此你会添加 HTML5 验证(type="email" 和 required 属性)。这对用户有帮助,但并不安全;任何人都可以打开 DevTools,修改输入类型,然后把无效数据发送到你的服务器。
为了真正保护你的应用,你需要手动进行验证:
export function validateSignup(input: SignupInput) {
const errors: Partial> = {};
if (!input.name || input.name.length ;
到底发生了什么?
- 我们使用 Zod 的简洁 API 定义了验证规则——不需要正则表达式。
z.infer可以从模式生成 TypeScript 类型,所以SignupInput始终与验证规则保持一致。
现在我们拥有一个 既能验证数据又能生成 TypeScript 类型 的模式。接下来,我们将在服务器端使用它。
4. 在服务器端进行验证
在 Next.js 中,我们可以使用 Server Actions(或 API 路由)来处理表单提交。此时验证至关重要——我们不能信任来自客户端的数据。
创建 app/actions/signup.ts:
"use server";
import { signupSchema, SignupInput } from "@/lib/schemas/signup";
export async function registerUser(data: unknown) {
// safeParse 返回对象而不是抛出异常
const result = signupSchema.safeParse(data);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
const validData: SignupInput = result.data;
// 这里你可以将数据保存到数据库
return {
success: true,
user: validData,
};
}
为什么使用 .safeParse() 而不是 .parse()?
.safeParse() 永远不会抛出异常;它返回 { success: boolean, data?: T, error?: ZodError }。这使得在 UI 代码中处理错误变得直观简洁。
我们的服务器现在已经通过单一的真相来源得到保护。
(可选)与 react‑hook‑form 结合使用
以后你可以使用 zodResolver 将该模式与 react-hook-form 集成,从而免费获得客户端验证。相同的模式将在客户端和服务器端共享,彻底消除重复代码。
Recap
- 一个 schema (
signupSchema) 一次性 定义验证规则。 - 该 schema 在运行时(客户端 & 服务器)以及编译时(TypeScript 类型)都能工作。
- 在服务器上使用
.safeParse()可以优雅地处理错误,而不会抛出异常。
使用 Zod,你可以消除重复的验证逻辑,减少 bug,并保持代码库清晰可维护。祝编码愉快!
无效数据会被明确的错误信息拒绝。现在我们来构建前端表单。
使用纯 React 构建表单
创建一个使用 Zod 进行验证的表单,但采用纯 React 状态管理。这展示了 Zod 如何独立工作——你不需要任何特殊的表单库。
文件:app/_auth/signup-form.tsx
"use client";
import { useState, FormEvent } from "react";
import { signupSchema, SignupInput } from "@/lib/schemas/signup";
import { registerUser } from "@/app/actions/signup";
export function SignupForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
website: "",
});
const [errors, setErrors] = useState>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e: React.ChangeEvent) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[name];
return newErrors;
});
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setErrors({});
// ✅ Validate with Zod (same schema as server!)
const result = signupSchema.safeParse(formData);
if (!result.success) {
// Convert Zod errors to a simple object
const fieldErrors: Record = {};
result.error.errors.forEach((error) => {
if (error.path[0]) {
fieldErrors[error.path[0].toString()] = error.message;
}
});
setErrors(fieldErrors);
setIsSubmitting(false);
return;
}
// Data is valid, send to server
const serverResult = await registerUser(result.data);
if (!serverResult.success) {
// Handle server‑side validation errors
setErrors(serverResult.errors || {});
setIsSubmitting(false);
return;
}
console.log("User registered successfully!", serverResult.user);
// Reset form
setFormData({
name: "",
email: "",
website: "",
});
setIsSubmitting(false);
};
return (
<form onSubmit={handleSubmit}>
<h2>Sign Up</h2>
{/* Name */}
<label>
Name
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
</label>
{errors.name && <p>{errors.name}</p>}
{/* Email */}
<label>
Email
<input
name="email"
value={formData.email}
onChange={handleChange}
/>
</label>
{errors.email && <p>{errors.email}</p>}
{/* Website */}
<label>
Website
<input
name="website"
value={formData.website}
onChange={handleChange}
/>
</label>
{errors.website && <p>{errors.website}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Signing up..." : "Sign Up"}
</button>
</form>
);
}
我们现在拥有一个可用的表单,它在客户端和服务器端都使用相同的 Zod 模式进行验证。它能够正确验证数据并显示清晰的错误信息。
然而,为状态处理和错误展示编写所有这些样板代码会变得繁琐。让我们简化它。
转向 React Hook Form
React Hook Form 是一个处理表单“繁琐”部分的库:跟踪输入值、显示错误以及管理提交状态。我们不需要编写大量的 useState 钩子,而是让库来完成这些工作。
要将它与 Zod 结合使用,我们使用 resolver —— 一个将 Zod schema 注入 React Hook Form 的桥梁,使其能够判断数据是否有效以及显示哪些错误信息。这样就不必在每次更改时手动验证。
了解更多关于 React Hook Form 的信息,请点击这里。
安装依赖
npm install react-hook-form @hookform/resolvers
在 React Hook Form 中使用 Zod
- 从
@hookform/resolvers/zod导入 resolver。 - 通过 resolver 将 Zod schema 传递给
useForm。 - 使用
register注册输入,让库处理值的变化和错误跟踪。
下一节(此处未展示)将演示使用 React Hook Form 的简洁实现。
React Hook Form 集成
使用 React Hook Form 时,我们只需连接我们的 schema 和输入字段。
我们将 signupSchema 传入 resolver 选项,使库使用我们的 Zod 规则。
不再需要大量的 useState Hook,我们使用 register 函数自动处理值和事件。这使代码更简洁、更小。
errors对象现在会自动显示 Zod 的错误信息。- 只有当所有数据均正确时,表单才会提交。
更新 app/_auth/signup-form.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { signupSchema, SignupInput } from "@/lib/schemas/signup";
import { registerUser } from "@/app/actions/signup";
export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm({
resolver: zodResolver(signupSchema), // ✅ Same schema as server!
});
const onSubmit = async (data: SignupInput) => {
const result = await registerUser(data);
if (!result.success) {
console.error("Validation errors:", result.errors);
return;
}
console.log("User registered successfully!", result.user);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Sign Up</h2>
{/* Name */}
<label>
Name
<input {...register("name")} />
</label>
{errors.name && <p>{errors.name.message}</p>}
{/* Email */}
<label>
Email
<input {...register("email")} />
</label>
{errors.email && <p>{errors.email.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Signing up..." : "Sign Up"}
</button>
</form>
);
}
回顾
- 我们在
lib/schemas/signup.ts中创建了一个 单一的 Zod schema,并在前端和后端共同使用。这意味着我们只需编写一次验证规则,修改规则后会自动在所有地方生效。 - React Hook Form 通过为我们管理表单状态,使代码更简洁,而
zodResolver则将其连接到我们的 schema。 - Zod 还能生成 TypeScript 类型,因此我们无需手动编写它们。
关键要点: 停止重复工作两次。使用 Zod 后,代码更安全、更易维护,且更不易出错。
停止重复你的验证逻辑。开始使用 Zod 吧! 🚀