メールアドレスのタイプミスをフィードバックするフォームをNext.jsで作る

メールアドレスのタイプミスをフィードバックするフォームをNext.jsで作る

前回のブログでは、Twilio SendGridの新機能「Email Address Validation API」の基本的な使い方について紹介しました。今回はその応用例として、入力されたメールアドレスを検証し、タイプミスがあった場合にフィードバックを返すお問い合わせフォームをNext.jsで構築します。

システムの概要図は以下のとおりです。基本的な構成は以前のブログ記事「Next.js App RouterとServer Actionsを使ってメールを送るフォームを作成する」と同じで、フォームのバリデーションとメールアドレスの検証機能が追加されます。初期セットアップや環境構築、デプロイ等の説明は上記の記事を参考にしてください。

システムの概要図

完成したアプリケーションは以下のようになります。
「gmalil.cpm」という誤ったドメインを入力してフォーム送信をすると、「gmail.com」の間違いではないかを確認するダイアログが表示されます。修正して再送信すると今度はダイアログが表示されずに送信が完了します。

完成したアプリケーション

アプリケーションの構成

今回利用する技術要素は以下の通りです。

  • フレームワーク:Next.js(利用バージョン:14.2.1)
    • App Routerを利用
    • TypeScriptでコーディングし、スタイリングにTailwind CSSを利用する設定
  • 型定義、フォームバリデーション:ZodReact Hook Form
  • メールアドレス検証とメール送信:SendGrid

このアプリケーションは以下の流れで処理を行います。

  1. フォームの表示とバリデーション
    名前、メールアドレス、お問い合わせ内容を記入できるフォームを表示します。それぞれのフィールドに文字が入力されると、Zodで定義した型チェックを行い、文字列ベースのバリデーションを行います。クライアントサイドバリデーションなので、エラーが発生した場合はリアルタイムで表示されます。
  2. 送信ボタンの活性化とAPI呼び出し
    型チェックに問題がなければ送信ボタンがクリックできる状態になります。送信ボタンがクリックされると、サーバーサイドでEmail Address Validation APIを呼び出します。タイプミスがあると予想される場合、またはドメインのメールサーバが存在しない場合は、その結果を元に画面上に警告メッセージを表示します。
  3. メール送信
    警告がない場合、もしくは警告があってもWebサイト訪問者が警告メッセージを確認後そのまま送信した場合は、そのメールアドレスにメールを送信します。

アプリケーションの構成

実装

型の定義

最初にお問い合わせフォームの入力内容を検証するための型を定義します。app/types.tsというファイルを作って、名前、メールアドレス、お問い合わせ内容のバリデーションルールを設定します。

import { z } from "zod";

export const schema = z.object({
	name: z
		.string({ required_error: "入力必須です" })
		.min(2, { message: "2文字以上で入力してください" }),
	email: z
		.string({ required_error: "入力必須です" })
		.email({ message: "メールアドレスの形式が正しくありません" }),
	content: z
		.string({ required_error: "入力必須です" })
		.min(2, { message: "2文字以上で入力してください" }),
});

export type Inquiry = z.infer<typeof schema>;

ここではZodというバリデーションライブラリを使用し、以下のルールを設定します。

  • 問い合わせ者の名前(name): 入力必須で、少なくとも2文字以上である必要がある
  • メールアドレス(email):入力必須で、正しいメールアドレス形式である必要がある
  • 問い合わせ内容(content): 入力必須で、少なくとも2文字以上である必要がある

これらのバリデーションはクライアントサイドで実行され、入力内容が型に反する場合は指定されたエラーメッセージが表示されます。
Email Address Validation APIでも正しいメールアドレスの形式であるかどうかのチェックは可能ですが、今回はクライアントサイドでチェックすることとし、Zodに任せています。

最後の行は、Zodの型定義を元にTypeScriptの型を生成しています。

サーバーサイドの関数定義

次に、サーバーサイドで実行する関数(Server Actions)を定義します。Email Address Validation APIを用いたメールアドレスの検証と、Mail Send APIによるメール送信の2種類です。

Email Address Validation APIによるメールアドレスの検証

app/actions.tsに「validateEmail」関数を定義します。この関数は、フォームに入力されたメールアドレスをもとにEmail Validation APIを呼び出して検証を行い、特定の条件に合致したときにのみ警告の文字列を返します。

"use server";
import type { Inquiry } from "./types";

// SendGrid Email Validation API
export async function validateEmail(
	email: string,
): Promise<{ warning: string | null }> {
	try {
		const response = await fetch(
			"https://api.sendgrid.com/v3/validations/email",
			{
				headers: {
					Authorization: `Bearer ${process.env.SENDGRID_VALIDATION_KEY}`,
				},
				method: "POST",
				body: JSON.stringify({
					email: email,
					source: "inquiry",
				}),
			},
		);
		const data = await response.json();
		if (response.ok) {
			let warning = null;
			if (data.result.suggestion) {
				const suggestedEmail = `${data.result.local}@${data.result.suggestion}`;
				warning = `メールアドレスは${suggestedEmail}ではありませんか?${data.result.email}のまま送信する場合はOKを押してください。`;
			} else if (data.result.checks.domain.has_mx_or_a_record === false) {
				warning = `「${data.result.email}」にタイプミスはありませんか?メールが届かない恐れがあります。このまま送信する場合はOKを押してください。`;
			}
			return { warning };
		}
		return { warning: null };
	} catch (e) {
		// 例外発生時は警告を表示しない
		console.error(e);
		return { warning: null };
	}
}
APIリクエストの送信(9-21行目)

fetch関数を使用してEmail Address Validation APIにPOSTリクエストを送信します。ヘッダにAPIキーを設定し、リクエストボディには検証したいメールアドレス(問い合わせフォームに入力されたメールアドレス)とsourceパラメータ(この場合は「inquiry」)を含めます。

検証結果の処理(23-32行目)

レスポンスが成功した場合、返されるデータを元に警告メッセージを生成します。具体的には以下のような処理を行います。

タイプミスのサジェスト(25-27行目)

メールアドレスのドメインにタイプミスが疑われる場合にのみ、レスポンスのJSONにresult.suggestionパラメータが含まれます。例えば「gmalil.cpm」ドメインを検証すると「gmail.com」がサジェストされるという具合です。ここでは、レスポンスにこのパラメータが含まれる場合にクライアントに提案する警告メッセージを生成しています。入力されたメールアドレスのローカルパートはresult.localパラメータで返されるので、これらを組み合わせればexample@gmalil.cpmのような入力に対してexample@gmail.comを提案できます。
具体的なAPIレスポンスの例は、前回のブログ記事をご参照ください。

ドメインのMXレコードの確認(28-30行目)

Email Address Validation APIではドメインにMXレコードやAレコードが存在しない場合、result.checks.domain.has_mx_or_a_recordパラメータがfalseとなります。宛先メールサーバがないことを意味するため、メールが届かないことが確実です。このケースも入力ミスがあった可能性が考えられるので、再確認してもらう警告文を生成しています。

エラーハンドリング(34-38行目)

APIレスポンスに予期しないエラーが発生してもクライアント側に影響を与えないようにしています。

メール送信

同じファイルに自動返信メールの送信処理を定義します。フォームの入力内容(名前、メールアドレス、問い合わせ内容)を受け取ってMail Send APIのリクエストを構成しています。Mail Send APIのパラメータ等の詳細についてはドキュメントをご覧ください。validateEmailで利用するAPIキーはEmail Address Validation API専用のものであるため、メール送信用に別のAPIキーを作成する必要があることに注意が必要です。

// SendGrid Mail Send API
export async function sendEmail(
	inquiry: Inquiry,
): Promise<{ success: boolean }> {
	try {
		const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
			method: "POST",
			headers: {
				Authorization: `Bearer ${process.env.SENDGRID_SEND_KEY}`,
				"Content-Type": "application/json",
			},
			body: JSON.stringify({
				personalizations: [
					{
						to: [
							{
								email: inquiry.email,
								name: inquiry.name,
							},
						],
					},
				],
				subject: "お問い合わせを受け付けました。",
				from: {
					email: "from@example.com",
				},
				content: [
					{
						type: "text/plain",
						value: `以下の内容でお問い合わせを受け付けました。\r\n------\r\n${inquiry.content}`,
					},
				],
			}),
		});
		if (response.ok) {
			console.log(response.status, response.statusText);
			return { success: true };
		}
		console.error(response.status, response.statusText);
		return { success: false };
	} catch (e) {
		console.error(e);
		return { success: false };
	}
}

フロントエンドのフォーム実装

最後に、フロントエンド部分でお問い合わせフォームを実装します。app/page.tsxに以下のように記述します。

"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { sendEmail, validateEmail } from "./actions";
import { type Inquiry, schema } from "./types";

export default function Contact() {
	const {
		register,
		handleSubmit,
		reset,
		setError,
		formState: { errors, isSubmitting, isSubmitSuccessful },
	} = useForm<Inquiry>({
		mode: "onChange",
		resolver: zodResolver(schema),
	});
	const onSubmit = handleSubmit(async (data) => {
		// Email Validation APIを使ってメールアドレスの形式をチェック
		const validation = await validateEmail(data.email);
		if (validation.warning) {
			const ignoreWarn = confirm(validation.warning);
			if (!ignoreWarn) {
				setError("email", { type: "cancel", message: "修正してください" });
				return;
			}
		}
		// メール送信
		const sendemail = await sendEmail(data);
		if (!sendemail.success) {
			setError("root", { type: "apiError", message: "送信に失敗しました" });
			return;
		}
		reset();
	});
	return (
		<div className="max-w-md mx-auto mt-4 bg-white p-6 rounded shadow-md">
			<h1 className="text-xl font-bold mb-4">お問い合わせフォーム</h1>
			<form onSubmit={onSubmit}>
				<div className="mb-4">
					<label
						htmlFor="name"
						className="block text-sm font-medium text-gray-600"
					>
						お名前
					</label>
					<input
						id="name"
						className="mt-1 p-2 w-full border rounded-md"
						{...register("name")}
					/>
					{typeof errors.name?.message === "string" && (
						<p className="text-red-500 text-sm">{errors.name.message}</p>
					)}
				</div>
				<div className="mb-4">
					<label
						htmlFor="email"
						className="block text-sm font-medium text-gray-600"
					>
						メールアドレス
					</label>
					<input
						id="email"
						type="email"
						className="mt-1 p-2 w-full border rounded-md"
						{...register("email")}
					/>
					{typeof errors.email?.message === "string" && (
						<p className="text-red-500 text-sm">{errors.email.message}</p>
					)}
				</div>
				<div className="mb-4">
					<label
						htmlFor="content"
						className="block text-sm font-medium text-gray-600"
					>
						お問い合わせ内容
					</label>
					<textarea
						id="content"
						rows={4}
						className="mt-1 p-2 w-full border rounded-md"
						{...register("content")}
					/>
					{typeof errors.content?.message === "string" && (
						<p className="text-red-500 text-sm">{errors.content.message}</p>
					)}
				</div>
				<div className="flex justify-center">
					<button
						type="submit"
						className={`px-8 py-2 rounded text-white ${
							isSubmitting
								? "bg-gray-500 cursor-not-allowed"
								: "bg-blue-600 hover:bg-blue-700"
						}`}
					>
						{isSubmitting ? "送信中..." : "送信"}
					</button>
				</div>
			</form>
			{isSubmitSuccessful && (
				<div className="flex flex-col items-center justify-center mt-4">
					<p>お問い合わせを受け付けました。</p>
					<p>自動返信メールをご確認ください。</p>
				</div>
			)}
			{typeof errors.root?.message === "string" && (
				<p className="flex justify-center text-red-500 mt-4">
					{errors.root.message}
				</p>
			)}
		</div>
	);
}
フォームのセットアップ(8-17行目)
  • React Hook FormのuseFormフックを使用してフォームの状態管理を行います
  • zodResolverを使用して、Zodスキーマによるバリデーションを設定します
  • modeにonChangeを指定して、入力されるたびにクライアントサイドバリデーションが行われるようにします
フォームの送信処理(18-35行目)
  • handleSubmitを使用してフォーム送信時の処理を定義しています
  • validateEmail関数を呼び出して、メールアドレスの検証をします。validateEmailから警告が返された場合はconfirmationダイアログを表示してWebサイト訪問者に知らせます
  • confirmationでキャンセルが選択された(フォーム送信が中止された)場合は、フォームのemailフィールドに修正を促す文言を表示します
  • 警告がない場合やconfirmationでOKが選択された(警告を無視してフォーム送信する)場合は、sendEmail関数を呼び出してメールを送信し、成功または失敗に応じてフォームの状態を更新します
フォームのUIの定義(36-115行目)
  • Tailwind CSSを使用してフォームのレイアウトを整えています
  • フォームフィールドにはregisterメソッドを使用して入力値をバインドし、エラーがある場合はエラーメッセージを表示します

以上で実装部分は完了です。APIキーの環境変数への登録を済ませれば、冒頭部分で紹介した動画のようなアプリが動作するはずです!

おわりに

最後に、Email Address Validation APIをWebサイトのフォームに組み込む際の注意点を述べます。

  • APIキーがクライアントサイドで読み込まれるとAPIを不正利用される恐れがあるため、サーバサイドで実行するように注意してください。今回はNext.jsのServer Actionsを使うことで、サーバサイドでリクエストを処理しました。
  • タイプミスが疑われるような場合でも、そのままフォーム送信を完了できる選択肢を残してください。フォーム送信を完全にブロックしてしまうとユーザ体験が損なわれる恐れがあります。修正の機会を与えつつ、最終的な判断をユーザに委ねるアプローチが望ましいでしょう。
  • Email Address Validation APIはProプラン以上で利用可能で、2,500回/月の無料枠がありますが、それ以上利用すると自動的に追加料金が発生します。実運用においては、いたずらに使用されないように適切な制限を設けてください。

Email Address Validation APIを使ったフィードバックを行えば、メールアドレスのタイプミスによりその先のフロー(会員登録等)に進めなくなるリスクを低減でき、機会損失の防止に役立つでしょう。また、フィードバックは行わず、メールアドレスの有効性だけチェックして、届かない宛先には送らないようにするという使い方でも、送信者のレピュテーション(宛先サーバからの評価)の維持に効果的です。ぜひご活用ください。

Email Address Validation APIの詳細は、前回のブログ記事ドキュメントAPIリファレンスをご参照ください。

アーカイブ

メールを成功の原動力に

開発者にもマーケターにも信頼されているメールサービスを利用して、
時間の節約、スケーラビリティ、メール配信に関する専門知識を手に入れましょう。