Next.jsとSupabaseでContact Formを実装しながら、APIとデータの流れを理解する

Published
2026-04-29
Author
MT
Tags

はじめに


どうも、Web制作の経験をベースに、

現在はエンジニアとして開発領域に取り組んでいる者です。


今回は、Next.js、Supabase、TypeScriptを使ってお問い合わせフォームを作成しながら、APIの実行からデータ保存までの一連の流れを理解することを目的に、バックエンド領域にも触れてみました。

きっかけとして、最近担当する領域が変わり、これまでのフロントエンド中心の業務から、バックエンドやデータ領域に携わるようになりました。

そこで今回は、フロントエンドからバックエンド、データベースまでの一連の流れを理解することを目的に、自分でテーブルを作成し、APIを実装し、それをフロントから叩いてバックエンドで処理し、DBにデータをINSERTするアプリを作成しました。


アプリの紹介


今回は、Next.js・Supabase・TypeScriptを使ってお問い合わせフォームアプリを作ってみました。


構成と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を使うことで、フロントエンドとAPIを同一プロジェクト内で扱えることを知り、localhost:3000 だけで一括管理できる点に魅力を感じました。

実際に自分で手を動かしたことで、Next.jsの利点が腑に落ち、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が呼び出されていることを確認できました。



こういった裏側の処理についても、今後はより深く理解し、実装できる領域を広げていきたいと考えています。


最後に

今回の実装を通して、「フロント→API→DB→レスポンス」という一連の流れを理解できたことで、システム全体を俯瞰して捉えられるようになってきたと感じています。

バックエンドに関しては、前職でお問い合わせフォームの保守運用を担当していたこともあり、基本的な処理の流れには実務を通じて触れてきました。今回の実装を通じて、これまでの知識や経験が点と点でつながり、APIやデータベースを含めたモダンな開発の流れをより深く理解できたと感じています。


これからも日々スキルを高めながら、少しずつ成長していきたいと思います。

それでは、また!