概要
Zod スキーマを一か所で定義し、フォームバリデーション(React Hook Form)と API レスポンス検証の両方で使い回す。 型定義と検証ロジックを重複させないことで、スキーマ変更時の修正コストを最小化できる。
インストール
npm install zod react-hook-form @hookform/resolvers
実装例
// src/lib/schemas/user.ts
import { z } from "zod";
// 共有スキーマ(フォームと API レスポンス両方で使う)
export const UserSchema = z.object({
id: z.number(),
name: z.string().min(1, "名前を入力してください"),
email: z.string().email("正しいメールアドレスを入力してください"),
});
// フォーム用(id は不要)
export const UserFormSchema = UserSchema.omit({ id: true });
// API レスポンス用(配列)
export const UserListSchema = z.array(UserSchema);
export type User = z.infer<typeof UserSchema>;
export type UserFormValues = z.infer<typeof UserFormSchema>;
// src/components/UserForm.tsx(フォームバリデーションで使う)
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { UserFormSchema, type UserFormValues } from "@/lib/schemas/user";
export function UserForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<UserFormValues>({
resolver: zodResolver(UserFormSchema),
});
const onSubmit = (data: UserFormValues) => {
console.log("送信:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<input
{...register("name")}
placeholder="名前"
className="w-full rounded border px-3 py-2 text-sm"
/>
{errors.name && (
<p className="mt-1 text-xs text-red-600">{errors.name.message}</p>
)}
</div>
<div>
<input
{...register("email")}
placeholder="メールアドレス"
className="w-full rounded border px-3 py-2 text-sm"
/>
{errors.email && (
<p className="mt-1 text-xs text-red-600">{errors.email.message}</p>
)}
</div>
<button
type="submit"
className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
>
送信
</button>
</form>
);
}
// src/lib/api/users.ts(API レスポンス検証で同じスキーマを使う)
import { UserListSchema, type User } from "@/lib/schemas/user";
export async function fetchUsers(): Promise<User[]> {
const res = await fetch("/api/users");
if (!res.ok) throw new Error("取得に失敗しました");
const json: unknown = await res.json();
return UserListSchema.parse(json);
}
ポイント
- スキーマを
src/lib/schemas/に分離することで、フォームと API の両方から import できる omit/pick/extendで用途ごとに派生スキーマを作ると、差分だけ管理できるz.infer<typeof Schema>で型を自動導出し、型定義を二重管理しない- フォームのバリデーションメッセージと API のエラーメッセージを同じスキーマで統一できる
- スキーマ変更時は一か所を修正するだけで、フォームと API 両方の型・検証が更新される