PrismaとZodで作る型駆動フォームバリデーション
〜DBスキーマとフォーム入力の一貫性を保つ〜
はじめに
フォームバリデーションとDBスキーマの整合性が取れていないと、
「入力は通るのにDB保存で落ちる」といったバグが発生しがちです。
ZodとPrismaを組み合わせることで、型定義を単一の真実(Single Source of Truth)として共有できます。
Prismaのスキーマ
// prisma/schema.prisma
model User {
id String @id @default(uuid())
name String
email String @unique
}
Zodスキーマを対応させる
// src/lib/validators/userSchema.ts
import { z } from "zod";
export const userSchema = z.object({
name: z.string().min(1, "名前は必須です"),
email: z.string().email("有効なメールアドレスを入力してください"),
});
export type UserInput = z.infer<typeof userSchema>;
サーバーアクションで検証を統合
// src/server/actions/createUser.ts
"use server";
import { prisma } from "@/lib/prisma";
import { userSchema } from "@/lib/validators/userSchema";
import { revalidatePath } from "next/cache";
export async function createUser(data: FormData) {
const parsed = userSchema.safeParse(Object.fromEntries(data));
if (!parsed.success) {
return { success: false, errors: parsed.error.flatten().fieldErrors };
}
const { name, email } = parsed.data;
await prisma.user.create({ data: { name, email } });
revalidatePath("/users");
return { success: true };
}
Zodによって入力検証と型変換が自動化され、DB保存時の安全性も確保されます。
フォームコンポーネントでエラーを表示
"use client";
import { useFormState } from "react-dom";
import { createUser } from "@/server/actions/createUser";
const initialState = { success: false, errors: {} };
export default function UserForm() {
const [state, formAction] = useFormState(createUser, initialState);
return (
<form action={formAction} className="flex flex-col gap-4">
<input name="name" placeholder="名前" className="border p-2" />
{state.errors?.name && <p className="text-red-500">{state.errors.name}</p>}
<input name="email" placeholder="メール" className="border p-2" />
{state.errors?.email && <p className="text-red-500">{state.errors.email}</p>}
<button type="submit" className="bg-blue-600 text-white py-2 rounded">
登録
</button>
</form>
);
}
まとめ
Zod × Prismaを組み合わせることで:
- 型定義の重複を防ぎ
- フォーム→DBまで一貫した安全性を確保
- 保守性の高い入力処理
を実現できます。
Next.jsのServer Actionsと合わせて使うと、さらに強力なフルスタック型駆動開発が可能です。