Next.js Server Actions + Zod でサーバーサイドバリデーション

スポンサーリンク

Next.js Server Actions + Zod でサーバーサイドバリデーション

Next.js の Server Actions でフォームデータを受け取るとき、Zod でバリデーションを入れると型安全に処理できます。クライアント・サーバーで同じスキーマを共有できるのが強みです。


早わかりまとめ

役割 実装場所
スキーマ定義 共有ファイル(lib/schemas.ts など)
フォームの送信 クライアントコンポーネント
バリデーション Server Actions(サーバー側)

インストール

npm install zod

スキーマを共有ファイルに定義する

// lib/schemas.ts
import { z } from 'zod'

export const contactSchema = z.object({
  name: z.string().min(1, { message: '名前を入力してください' }),
  email: z.string().email({ message: 'メールアドレスの形式が正しくありません' }),
  message: z.string().min(10, { message: '10文字以上入力してください' }),
})

export type ContactFormValues = z.infer<typeof contactSchema>

Server Action でバリデーションする

// app/actions/contact.ts
'use server'

import { contactSchema } from '@/lib/schemas'

export async function submitContact(formData: FormData) {
  const raw = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  }

  const result = contactSchema.safeParse(raw)

  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }

  // result.data は型安全:ContactFormValues
  const { name, email, message } = result.data

  // DB保存やメール送信などの処理
  // ...

  return { success: true }
}

フォームコンポーネントから呼び出す

// app/contact/page.tsx
'use client'

import { useActionState } from 'react'
import { submitContact } from '@/app/actions/contact'

const initialState = { success: false, errors: {} }

export default function ContactPage() {
  const [state, action] = useActionState(submitContact, initialState)

  return (
    <form action={action}>
      <div>
        <input name="name" placeholder="名前" />
        {state.errors?.name && <p>{state.errors.name[0]}</p>}
      </div>

      <div>
        <input name="email" type="email" placeholder="メールアドレス" />
        {state.errors?.email && <p>{state.errors.email[0]}</p>}
      </div>

      <div>
        <textarea name="message" placeholder="メッセージ" />
        {state.errors?.message && <p>{state.errors.message[0]}</p>}
      </div>

      <button type="submit">送信</button>

      {state.success && <p>送信しました</p>}
    </form>
  )
}

error.flatten() でエラーを整形する

const result = schema.safeParse(data)

if (!result.success) {
  // fieldErrors: フィールドごとのエラー配列
  // formErrors: フォーム全体のエラー配列
  const { fieldErrors, formErrors } = result.error.flatten()
  console.log(fieldErrors)
  // { name: ['名前を入力してください'], email: ['...'] }
}

クライアントとサーバーでスキーマを共有するメリット

// lib/schemas.ts(共有)
export const contactSchema = z.object({ ... })
export type ContactFormValues = z.infer<typeof contactSchema>

// クライアント側(react-hook-form と組み合わせる場合)
import { contactSchema, ContactFormValues } from '@/lib/schemas'
useForm<ContactFormValues>({ resolver: zodResolver(contactSchema) })

// サーバー側(Server Action)
import { contactSchema } from '@/lib/schemas'
contactSchema.safeParse(formData)

スキーマを1か所に定義することで、クライアント・サーバーのバリデーションルールが常に一致します。


まとめ

やること コード
スキーマ共有ファイルに定義 lib/schemas.ts
Server Action でバリデーション schema.safeParse(formData)
エラーを整形する result.error.flatten().fieldErrors
フォームにエラーを表示 useActionState で state を受け取る
クライアント側にも使い回す zodResolver(schema) で共有

Zod の基本的な使い方は「Zod 入門:z.object・z.infer・safeParse の使い方」を参照してください。