目次
はじめに
どうも、Web制作の経験をベースに、
現在はエンジニアとして開発領域に取り組んでいる者です。
今回は、Next.js と Supabase を使ってお問い合わせフォームを作成しながら、APIの実行からデータ保存までの一連の流れを理解することを目的に、バックエンド領域にも触れてみました。
アプリの紹介
今回は、Next.js・Supabaseを使ってお問い合わせフォームアプリを作ってみました。
👉 アプリはこちら
https://contact-form-app-beryl.vercel.app/contact
構成とNext.jsを選んだ理由
今回のアプリは、以下のような構成をイメージして設計しました。
/contact(入力画面)
↓
POST /api/contact/validate(入力内容のバリデーション)
↓
/ contact/confirm(確認画面)
↓
POST /api/contact(本送信API)
↓
Supabaseに保存
↓
/ contact/thanks(完了画面)
当初はReact単体で実装しようと考えていましたが、Reactはクライアントサイドで動作するSPAであるため、バックエンドを利用する場合は、フロントエンド(localhost:5173)とバックエンド(localhost:3001)を分けて起動・連携する必要がありました。
そこで、フロントエンドとバックエンドを別々に管理する構成を避け、よりシンプルな構成にするためNext.jsを採用しました。 Next.jsでは、フロントエンドとAPIを同一プロジェクト内で扱えるため、開発環境をlocalhost:3000にまとめられる点も採用理由のひとつです。
Next.jsでは、ディレクトリ構成に基づいてAPIエンドポイントを定義できるため、フロントエンドとAPIの関係も把握しやすいと感じています。
また、両者を同一プロジェクト内で管理できる点は、構成をシンプルに保つうえでもメリットがあると考えています。
フロントからAPI・DBまでの一連の流れ
ここからは、実際の実装の流れについて説明します。
入力画面
まず入力画面には、name・email・message の3項目を用意しています。
このうち、name と email は必須項目としており、ボタンを押下するとフロント側から /api/contact/validate にPOSTリクエストを送信し、サーバー側でバリデーションを実行しています。
入力内容に不備がある場合は、API側でエラーとして処理され、エラーレスポンスを返却し、フロント側でエラーメッセージを表示する構成としています。
バリデーションでは、以下のように入力値をチェックしています。
- name が空でないか
- email が空でないか
- email に @ が含まれているか
エラーがある場合は、以下のようなレスポンスを返却します。
{
errors: {
name: "Please enter your name.",
email: "Please enter your email address."
}
}
API側では errors オブジェクトにエラーが1つ以上存在する場合、HTTPステータスコード400でエラーレスポンスを返却しています。
if (Object.keys(errors).length > 0) {
return Response.json({ errors }, { status: 400 });
}
バリデーションに成功した場合は、入力内容を sessionStorage に保存し、確認画面へ遷移します。
sessionStorage.setItem('contactForm', JSON.stringify(form));
router.push('/contact/confirm');
確認画面
確認画面に遷移後、APIから ok: true が返却されていることを確認。
入力画面で sessionStorage に保存していた内容を取得し、画面に表示しています。
const saved = sessionStorage.getItem('contactForm');
if (!saved) {
router.push('/contact');
return;
}
setForm(JSON.parse(saved));
ブラウザの開発者ツールを用いて、sessionStorage に値が格納されていることを確認。
その後、「Send Message」ボタンを押下すると、最終的な送信処理として /api/contact にPOSTリクエスト を送信します。
SupabaseへのデータINSERT処理
/api/contact では、再度バリデーションを行った上で、Supabaseのクライアントを用いてデータベースにINSERT処理を行っています。
const { error } = await supabase.from('contacts').insert([
{
name,
email,
message: message || '',
},
]);
実際にSupabaseのテーブルを確認すると、送信したデータが正常にINSERTされていることを確認できました。
完了画面
INSERTが正常に完了した場合、APIからは、以下のようなレスポンスが返却されます。
{
"ok": true,
"message": "Request successful"
}
フロント側では、HTTPステータスが200であることを確認した上で、sessionStorage に保存していた入力内容を削除し、完了画面へ遷移します。
sessionStorage.removeItem('contactForm');
router.push('/contact/thanks');
以上が一連の流れになります。
本来であれば、送信後にデータベースへINSERTするだけでなく、送信者への確認メールや管理者への通知メールを送る機能なども必要だと考えています。
ただし、今回は実際の運用を想定したアプリではないため、そこまでの機能は実装していません。
余談:バッチ処理の実装について
今回の実装では、Supabaseの無料プランを使っており一定期間アクセスがないとプロジェクトが一時停止される可能性があるため、GitHub Actionsを使って定期的にAPIを実行する仕組みを導入しています。
定期処理用のエンドポイントを用意し、Supabaseのテーブルに対して負荷の軽いSELECTクエリを実行することで、定期的にデータベースへアクセスしています。
また、GitHub Actionsから手動でエンドポイントを実行し、Vercelのログを確認したところ、実際にAPIが呼び出されていることを確認できました。
こういった裏側の処理についても、今後はより深く理解し、実装できる領域を広げていきたいと考えています。
それでは、また!