フロントエンド 2026.08.14

Next.js + TypeScriptで問い合わせフォームを型安全に実装する実践ガイド

約13分で読めます

型エラーや不正データに悩むエンジニア向けに、Next.js + TypeScriptでの問い合わせフォーム実装を実案件ベースで解説。Server ActionsとZodを組み合わせた型安全な設計を詳しく紹介します。

「フォームのバリデーション、どこまで書けばいいの?」

こんな悩みはありませんか?

  • サーバー側とクライアント側でバリデーションロジックが二重管理になってしまっている
  • フォームの送信データに予期しない型が混入してビルドエラーが頻発する
  • any を使ってとりあえず動かしているが、後で見直すたびに何の値か分からなくなる
  • React Hook Form を導入したものの、Server Actions との連携方法が分からず手が止まっている

これはフロントエンド開発の現場でよく耳にする声です。特にNext.js 13以降でApp Routerが登場してから、フォーム処理のベストプラクティスが大きく変わり、「以前の方法が通じなくなった」と混乱しているエンジニアも少なくありません。

この記事では、Fivenineが実際の案件で採用している「Next.js + TypeScript + Zod + React Hook Form」の組み合わせを、実装の意図も含めて丁寧に解説します。読み終えたあとには、型安全なフォーム設計の全体像が掴めるようになっています。


なぜ「型安全なフォーム」が重要なのか

フォームは一見シンプルに見えますが、実はアプリケーションの中でもっとも多くの不正入力が流入する場所です。ユーザーが意図せず入力する半角・全角の混在、空白文字の混入、予期しないフィールドの欠落——これらをサーバー側だけで防ごうとすると、デバッグが難しい実行時エラーに悩まされます。

TypeScriptを使っているからといって、それだけでは外部から来るデータ(=フォームの送信値)は型安全になりません。TypeScriptの型はコンパイル時にしか機能しないため、ランタイムで流れ込んでくる文字列や数値は「信頼できないデータ」として扱う必要があります。

ここで鍵になるのが Zodによるスキーマバリデーション です。Zodはランタイムで型の検証を行い、TypeScriptの型推論とも完全に連携します。つまり「一度スキーマを定義すれば、型定義とバリデーションを同時に手に入れられる」という強力な仕組みが実現します。

flowchart TD
    A[ユーザーがフォーム入力] --> B[React Hook Form でクライアントバリデーション]
    B -->|エラーあり| C[UIにエラー表示]
    B -->|OK| D[Server Action に送信]
    D --> E[Zod でサーバーサイドバリデーション]
    E -->|エラーあり| F[エラーをクライアントに返す]
    E -->|OK| G[メール送信 / DB保存]
    G --> H[完了ページへリダイレクト]

フロントエンド開発をお探しですか?

React・Vue・モダンなUI/UX開発をサポートします

無料で相談する

実案件で採用した構成と実装手順

背景:神奈川県内の製造業クライアントの事例

あるクライアントでは、既存のWordPressサイトから問い合わせフォームだけをNext.jsに切り出して再実装するプロジェクトを受注しました。課題は「スパム送信が多発していること」と「送信後の処理ロジックが管理しにくいこと」の2点でした。

以前の実装はJavaScript(素のJS)でバリデーションを行い、PHPのメール送信スクリプトに投げる構成でしたが、型の保証が一切なく、不正なデータが混入しても気づけない状態でした。この課題を解決するために、Next.js App Router + TypeScript + Zod の構成を選択し、開発期間2週間で安定した動作を実現しました。

Step 1: Zodでスキーマを定義する

まずフォームのデータ構造をZodスキーマとして一元管理します。このスキーマが「唯一の真実(Single Source of Truth)」となります。

// lib/schema/contact.ts
import { z } from 'zod';

export const contactSchema = z.object({
  name: z
    .string()
    .min(1, 'お名前を入力してください')
    .max(50, 'お名前は50文字以内で入力してください'),
  email: z
    .string()
    .email('正しいメールアドレスを入力してください'),
  phone: z
    .string()
    .regex(/^[0-9-+()\s]*$/, '電話番号の形式が正しくありません')
    .optional(),
  message: z
    .string()
    .min(10, 'お問い合わせ内容は10文字以上入力してください')
    .max(1000, 'お問い合わせ内容は1000文字以内で入力してください'),
});

// TypeScriptの型をスキーマから自動生成
export type ContactFormData = z.infer<typeof contactSchema>;

z.infer<typeof contactSchema> の一行で型定義が自動生成されます。スキーマを更新すれば型定義も自動的に追従するため、「バリデーションと型の定義がズレていた」という古典的なバグが構造的に発生しなくなります。

Step 2: Server Actionを実装する

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

import { contactSchema } from '@/lib/schema/contact';
import { z } from 'zod';

type ActionResult =
  | { success: true }
  | { success: false; errors: z.ZodFormattedError<ContactFormData> };

export async function submitContact(
  data: unknown
): Promise<ActionResult> {
  // ランタイムでスキーマ検証
  const parsed = contactSchema.safeParse(data);

  if (!parsed.success) {
    return {
      success: false,
      errors: parsed.error.format(),
    };
  }

  // ここからは parsed.data が型安全に使える
  const { name, email, message } = parsed.data;

  // メール送信処理(nodemailerなど)
  await sendEmail({ name, email, message });

  return { success: true };
}

safeParse を使うことで、バリデーション失敗時も例外ではなくオブジェクトとして結果が返るため、エラーハンドリングが格段に書きやすくなります。

Step 3: React Hook FormとZodを連携する

// components/ContactForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { contactSchema, ContactFormData } from '@/lib/schema/contact';
import { submitContact } from '@/app/actions/contact';
import { useState } from 'react';

export function ContactForm() {
  const [isSubmitted, setIsSubmitted] = useState(false);

  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema), // スキーマをそのまま渡すだけ
  });

  const onSubmit = async (data: ContactFormData) => {
    const result = await submitContact(data);
    if (result.success) {
      setIsSubmitted(true);
    }
  };

  if (isSubmitted) {
    return <p>お問い合わせを受け付けました。担当者よりご連絡いたします。</p>;
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">お名前 *</label>
        <input id="name" {...register('name')} />
        {errors.name && <p>{errors.name.message}</p>}
      </div>

      <div>
        <label htmlFor="email">メールアドレス *</label>
        <input id="email" type="email" {...register('email')} />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

      <div>
        <label htmlFor="message">お問い合わせ内容 *</label>
        <textarea id="message" {...register('message')} />
        {errors.message && <p>{errors.message.message}</p>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : '送信する'}
      </button>
    </form>
  );
}

zodResolver(contactSchema) の一行を渡すだけで、React Hook FormがZodスキーマに基づいてクライアントサイドのバリデーションを自動で行います。別途バリデーションルールを書く必要はありません。


よくある失敗パターンと対処法

実案件でも何度か踏んだ落とし穴を正直にお伝えします。

失敗1:Server Action に渡す前に型アサーションを使う

data as ContactFormData のようなキャストを使ってしまうと、TypeScriptの型チェックを騙しているだけでランタイムの安全性はゼロです。必ず safeParse を通してください。

失敗2:スキーマをクライアントとサーバーで別々に定義する

「クライアント用バリデーション」と「サーバー用バリデーション」を別ファイルで管理しているケースをよく見かけますが、両者が徐々にズレていき、片方だけで通過するデータが生まれます。lib/schema/ に一元化するのが鉄則です。

失敗3:エラーメッセージを日本語化し忘れる

Zodのデフォルトエラーメッセージは英語です。本番環境でユーザーに「Required」と表示されてしまったケースが実際にありました。スキーマ定義時にメッセージを必ず日本語で記述する習慣をつけてください。

失敗4:送信中の二重送信対策を忘れる

isSubmitting フラグを使ってボタンを disabled にする処理は、シンプルですが必須です。二重送信が発生するとメールが複数回送られ、クライアントからの信頼を失います。


フロントエンド開発をお探しですか?

React・Vue・モダンなUI/UX開発をサポートします

無料で相談する

Web制作・UI改善もお任せください

フロントエンド開発

モダンな技術で、使いやすく美しいWebサイトを実現します

200件以上の制作実績 顧客満足度97% 初回相談無料

※ 通常1営業日以内にご返信します

まとめと次のステップ

今回紹介した構成のポイントは「スキーマを中心に置いて、型とバリデーションを一元管理する」という設計思想です。ZodスキーマはTypeScriptの型定義にもなり、クライアントのリアルタイムバリデーションにもなり、サーバーのランタイム検証にもなる——この「一石三鳥」の仕組みが、Next.js + TypeScriptの組み合わせを強力にしている理由です。

自社サイトへの導入を検討している方は、まず小さなフォームから試してみることをおすすめします。全体のアーキテクチャを変える必要はなく、一つのフォームコンポーネントから始められます。

Fivenineでは、既存サイトへの問い合わせフォーム単体の実装から、Next.jsを活用した大規模なリプレイスまで幅広く対応しています。「自社の場合はどう設計すればいいか分からない」という段階からでも、ぜひお気軽にご相談ください。

この記事をシェア

Webサイトの改善、お任せください

デザイン改善・表示速度向上・レスポンシブ対応など、成果の出るWeb制作を行います。 初回相談は無料です。

※ 1営業日以内にご返信いたします

この技術でお困りなら

無料でプロに相談できます

相談する
AIに無料相談