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 の使い方」を参照してください。