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を組み合わせることで:

を実現できます。
Next.jsのServer Actionsと合わせて使うと、さらに強力なフルスタック型駆動開発が可能です。