メインコンテンツへスキップ
見出し画像

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`) で状態を受け取り、ユーザーにフィードバックする。

    この分離が、メンテナンス性と堅牢性を劇的に高めます。

    では、具体的な実装コードを見ていきましょう。
    ここからは、僕が実際の案件や個人開発で使い回している「テンプレート」レベルの実装パターンを公開します。

     
     
    15年のフリーランス / ソフトウェアエンジニア経験を基に、仕事が劇的にラクになる思考法を発信し、あなたの生産性と心の余裕を生み出します。吃音で選んだ物語の力で、人生の深い葛藤を乗り越えるヒントと癒しを提供。フォローで専門知識と心の拠り所となる物語が手に入ります。

    あなたへのおすすめ