第278章:Server Actions での入力検証🛡️
この章では「フォーム送信 → Server Action → Valibotで検証 → エラーを画面に表示」を1セットで作るよ〜💌😊 Server Actions はサーバーで動く関数だから、最終防衛ラインとしてここで必ず検証するのが超大事だよ🧱🔥 (Next.js)
今日つくるもの🎯
お問い合わせフォーム(名前・メール・メッセージ)📮 入力ミスがあったら、その場で優しくエラー表示してあげるやつ🥰
図解:フォーム送信の流れ📨➡️🛡️➡️🖥️

Server Actions は POSTで呼ばれるのが基本だよ〜📮 (Next.js)
1) Valibot を入れる🐼📦
PowerShell(またはVSCodeのターミナル)で👇
npm i valibot
2) Server Action(サーバー側)を書く🧑🍳🛡️
app/actions/contact.ts を作成✨
ポイントはこれ👇
- ファイル先頭に
"use server"(このファイルのexportがサーバー関数になるよ) (Next.js) - Valibot の
safeParse(schema, input)で 落とさずに結果を受け取る (Valibot) - エラーは返して、UIで表示する🫶
'use server'
import * as v from 'valibot'
const ContactSchema = v.object({
name: v.pipe(
v.string(),
v.trim(),
v.minLength(1, 'お名前を入力してね😊'),
v.maxLength(30, 'お名前は30文字までだよ✍️')
),
email: v.pipe(
v.string(),
v.trim(),
v.nonEmpty('メールアドレスが空っぽだよ📮'),
v.email('メールの形がちょっと変かも…🤔'),
v.maxLength(100, 'メール長すぎるかも💦')
),
message: v.pipe(
v.string(),
v.trim(),
v.minLength(10, 'メッセージは10文字以上でお願い🙏'),
v.maxLength(300, 'メッセージは300文字までだよ📝')
),
})
export type ContactState = {
ok: boolean
message: string
fieldErrors?: {
name?: string[]
email?: string[]
message?: string[]
_root?: string[]
}
}
export async function sendContact(
prevState: ContactState,
formData: FormData
): Promise<ContactState> {
// FormDataは null や File の可能性もあるから、まず文字列に寄せるよ🧼
const raw = {
name: String(formData.get('name') ?? ''),
email: String(formData.get('email') ?? ''),
message: String(formData.get('message') ?? ''),
}
const result = v.safeParse(ContactSchema, raw)
if (!result.success) {
// 画面表示しやすい形にする(詳しくは後の章で深掘りするよ😉)
const flat = v.flatten<typeof ContactSchema>(result.issues)
return {
ok: false,
message: '入力をもう一回チェックしてね🙏',
fieldErrors: {
name: flat.nested?.name,
email: flat.nested?.email,
message: flat.nested?.message,
_root: flat.root,
},
}
}
// 検証OKなら、result.output が「型安全なデータ」✨
const data = result.output
// ここでDB保存やメール送信などをする想定📦✉️
console.log('contact:', data)
return {
ok: true,
message: '送信できたよ〜!ありがとう💌',
}
}
pipe,trim,emailみたいに “つなげて検証”できるのが Valibot の気持ちよさだよ🐼✨ (Valibot)flattenは issues を “表示向け” に平坦化してくれるよ🧾 (Valibot)
3) フォームUI(クライアント側)を書く🧸🖥️
エラー表示したいから、フォーム側は Client Component にするよ〜🎮
useActionState を使うと、**state(結果)とpending(送信中)**を一緒に扱えて便利✨ (Next.js)
app/contact/page.tsx
'use client'
import { useActionState } from 'react'
import { sendContact, type ContactState } from '@/app/actions/contact'
const initialState: ContactState = {
ok: false,
message: '',
}
export default function ContactPage() {
const [state, formAction, pending] = useActionState(sendContact, initialState)
return (
<main style={{ maxWidth: 560, margin: '40px auto', padding: 16 }}>
<h1 style={{ fontSize: 24, fontWeight: 700 }}>お問い合わせ📮</h1>
<p style={{ marginTop: 8, opacity: 0.8 }}>気軽に送ってね〜😊</p>
<form action={formAction} style={{ marginTop: 20, display: 'grid', gap: 12 }}>
<div>
<label>お名前</label>
<input name="name" type="text" style={inputStyle} />
{state.fieldErrors?.name?.map((msg, i) => (
<p key={i} style={errorStyle}>⚠️ {msg}</p>
))}
</div>
<div>
<label>メール</label>
<input name="email" type="text" style={inputStyle} />
{state.fieldErrors?.email?.map((msg, i) => (
<p key={i} style={errorStyle}>⚠️ {msg}</p>
))}
</div>
<div>
<label>メッセージ</label>
<textarea name="message" rows={5} style={inputStyle} />
{state.fieldErrors?.message?.map((msg, i) => (
<p key={i} style={errorStyle}>⚠️ {msg}</p>
))}
</div>
{state.message && (
<p aria-live="polite" style={{ marginTop: 4 }}>
{state.ok ? '✅ ' : '⚠️ '}
{state.message}
</p>
)}
<button disabled={pending} type="submit" style={buttonStyle}>
{pending ? '送信中…⏳' : '送信する💌'}
</button>
</form>
</main>
)
}
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '10px 12px',
borderRadius: 10,
border: '1px solid #ccc',
marginTop: 6,
}
const errorStyle: React.CSSProperties = {
marginTop: 6,
fontSize: 13,
}
const buttonStyle: React.CSSProperties = {
marginTop: 8,
padding: '10px 12px',
borderRadius: 12,
border: 'none',
cursor: 'pointer',
}
useActionState の pending で「送信中はボタン無効化」できるよ〜🫶 (Next.js)
4) 動かして確認しよ〜🚀
npm run dev
http://localhost:3000/contactにアクセス🌈- わざと空で送って、エラーが出るか確認👀⚠️
- 正しく入れて送って、成功メッセージも確認🎉
よくあるハマりどころ🪤(ここだけ押さえたら勝ち✌️)
- Server Action 側に
"use server"がない → ただの関数になっちゃう🥲 (Next.js) - エラー表示したいのにフォームがServer Componentのまま →
useActionStateが使えないのでuse clientにする🎮 (Next.js) Object.fromEntries(formData)を使うと楽だけど、$ACTION_で始まる余計なキーが混ざることがあるよ(複数フィールドだと注意)🧠 (Next.js)
ミニ練習🎀(5分でOK)
フォームに「件名 subject」を足してみてね💡
- 1文字以上
- 50文字以内
- エラー表示も追加✨
(できたら次章で、エラーメッセージ整形をもっと気持ちよくするよ〜😉)