Next.js のフォーム実装、もう 「事故」 らせない。 Server Actions × Zod で築く鉄壁の設計
バグに怯える夜にサヨナラを
「お問い合わせフォームを作ってください」
クライアントからそう言われたとき、あなたはどう感じますか?
「はい、簡単です!」と即答できるでしょうか。
それとも、一瞬だけ心の奥に重たい石が落ちるような感覚があるでしょうか。
かつての僕は、後者でした。
見た目はただの入力欄とボタン。
けれど、その裏側には無数の「落とし穴」が潜んでいることを知っているからです。
必須項目の抜け漏れ、不正なメールアドレス、サーバーエラー時の挙動、二重送信、そしてローディング中の UI 制御……。
これらを一つひとつ手作業で潰していく作業は、まるで地雷撤去のようでした。
「動くけれど、どこか不安」
そんなコードを本番環境にデプロイする夜は、正直言って生きた心地がしませんでした。
しかし、Next.js の Server Actions とスキーマ検証ライブラリ Zod に出会ってから、その景色は一変しました。
フォーム実装は「怖いもの」から、「型に守られた、堅牢なパズル」 へと進化したのです。
この記事では、僕が数々の失敗を経てたどり着いた、「もう二度と事故らせない、鉄壁のフォーム設計」 を共有します。
useEffect と API Routes の迷宮
少し前までの僕のフォーム実装は、正直言って「継ぎ接ぎだらけ」でした。
Pages Router 時代、あるいは React の `useEffect` を多用していた頃のコードを思い出すと、今でも冷や汗が出ます。
散らばるバリデーション:
クライアント側で JavaScript でチェックし、API Route 側でも同じようなチェックを書く二重管理。
修正漏れが頻発しました。
肥大化するステート:
`const [name, setName] = useState('')` ...
入力項目が増えるたびに増殖する `useState`。
そして `isLoading`, `isError`, `successMessage`...
管理しきれない状態変数たち。
型安全性の欠如:
API から返ってくるエラーレスポンスの型が曖昧で、フロントエンドで `any` で受けてしまう妥協。
結果として出来上がるのは、「動いているけれど、誰も触りたくないスパゲッティコード」でした。
仕様変更が入るたびにどこかが壊れ、バリデーションをすり抜けた不正データがデータベースに保存されてしまう——そんな「事故」が、忘れた頃にやってくるのです。
サーバーとクライアントを 「型」 で繋ぐ
転機は、Next.js App Router の本格導入と共に訪れました。
Server Actions の登場です。
これにより、フォームの送信処理を「関数」として定義し、それを直接コンポーネントから呼べるようになりました。
API エンドポイントを意識する必要がなくなり、サーバー側のロジックとクライアント側の UI がシームレスに繋がったのです。
そして、そこに Zod という最強の盾を組み合わせる。
これが、僕が見つけた「鉄壁」の正体でした。
バリデーションの一元化:
Zod スキーマを一つ定義すれば、それをサーバー側の検証にも、クライアント側の型定義にも使い回せる。
プログレッシブ・エンハンスメント:
JavaScript がオフでも最低限動作する堅牢な基盤。
型の導き:
入力値も、返却されるエラーメッセージも、すべて TypeScript の型が守ってくれる。
「あ、これなら事故りようがない」
初めてこの構成で実装し終えたとき、そう確信しました。
コード量は減っているのに、安心感は何倍にも増していたのです。
3 つのレイヤー
この(鉄壁の)設計は、大きく分けて 3 つのレイヤーで構成されています。
Definition Layer (定義層):
Zod で「正しいデータ」の形を定義する。
Action Layer (処理層):
Server Action で入力を検証し、結果(成功/失敗)を返す。
UI Layer (表示層):
`useActionState` (旧 `useFormState`) で状態を受け取り、ユーザーにフィードバックする。
この分離が、メンテナンス性と堅牢性を劇的に高めます。
では、具体的な実装コードを見ていきましょう。
ここからは、僕が実際の案件や個人開発で使い回している「テンプレート」レベルの実装パターンを公開します。
ここから先は
最後まで読んでいただき、ありがとうございます。もしこの記事が「役に立った」「心に響いた」と感じたら、珈琲一杯分サポートいただけると嬉しいです。あなたの温かい応援を力に、また次の創作活動に励みます。
