API firstのメール基盤:Resendがメール認証に選ばれるシンプルな理由のサムネイル

はじめに

メール認証の実装でこんなことありませんか?

自前認証を実装することになり、登録確認メールとパスワードリセットメールの送信機能も自分で作らなければならなくなった。
SendGridやSESを調べたが、APIキーの設定・IPウォームアップ・バウンス管理など、メール送信を運用する複雑さに圧倒された。
Edge Runtime上から呼び出せるシンプルなメール送信APIがほしいが、適切なサービスの選び方がわからなかった。

この記事をお勧めしない人

  • すでにSendGridやSESを使っており、メール基盤の移行を検討していない人。
  • バルクメール(マーケティングメール)の大量配信が主目的で、トランザクショナルメールが不要な人。
  • メールテンプレートをHTMLファイルで管理することに問題がなく、React Emailへの移行に関心がない人。

もし一つでも当てはまらないなら、読み進める価値があるかもしれません。

メール送信の複雑さが認証基盤全体の足を引っ張る

  • SMTPサーバーの設定やIPウォームアップの管理が必要になり、メール配信の設定に想定外の工数がかかる。
  • バウンスや迷惑メール報告の処理をWebhookで受け取る実装が増え、認証フローとは無関係なコードが膨らむ。
  • Edge Runtimeで動かないNode.js依存のメールライブラリを使い続けることで、認証基盤全体の環境依存が広がる。

APIを呼ぶだけで送れるという明るい未来

  • この記事を読めば、HTTP POST一発でメールを送信できるResendを使い、メール基盤の運用コストをほぼゼロにできる明るい未来があります。
  • 具体的には、React Emailでテンプレートを型安全に管理し、Edge Runtimeから直接呼び出すボイラープレートを手に入れられます。
  • この方法は、筆者が運営するReact Router × Cloudflare Edgeのブログのメール認証基盤として実際に稼働しています。
  • この情報は、「Cloudflare Workers + 自前認証 + Resend」という組み合わせの実践知として、巷の記事では決して得られない実装の勘所を含んでいます。

このブログも同じ選定をした

メール認証基盤を設計する過程で、SendGridの複雑さとnodemailerのEdge非対応という2つの壁に当たりました。そこで「HTTPを話せる環境から呼べる」という条件だけで選んだのがResendです。この記事では、その判断の根拠とClaudeMixでの実装パターンを持ち帰れるように書きました。D1・KVの記事と合わせて読むと、Cloudflareネイティブ層の全体像が見えてきます。なお、D1(ユーザー)・KV(セッション)・Resend(メール)の組み合わせは、実質的に「Build your own Clerk」の最小構成です。認証SaaSを使わずEdgeで自前Authを設計する記事は日本語圏でほぼ存在しない——この記事シリーズの希少性はそこにあります。

延命医の診断

メール送信の本質はHTTPである

SMTPは1970年代から続くプロトコルで、TCP接続・stateful・queue・retryを前提とした設計だ。つまり【メールサーバー運用前提】。SendGridやSESの複雑さ(IPウォームアップ・バウンス管理・SMTP relay)は設計思想の問題ではなく、SMTP世界をそのままAPI化した結果だ。

Cloudflare Workers(Edge Runtime)はTCP socketを直接開けない。HTTPフェッチしか使えない。だから【Edge時代のメールAPIはHTTPしかありえない】。Resendはこの構造を突いた——SMTPを裏側に押し込め、開発者にはHTTPだけを見せる。

Workers → HTTP → Resend → SMTP(裏側)

「HTTPを話せる環境ならどこからでも送れる」。これがEdgeとResendの整合性が構造的に保証される理由だ。

SendGrid → SES → Resendという淘汰の文脈

SendGridはメールインフラを提供した第1世代。SESはAWSのエコシステムにメールを組み込んだ第2世代。Resendは「開発者がAPIとして使いたい」という需要だけを切り出した第3世代だ。2023年創業ながら急速に開発者コミュニティに広がったのは、過剰な機能を削ぎ落として1つのことだけを正しくやっているから。React Emailとの公式統合もこの思想の延長にある。

React Emailとの接合点

HTMLメールはWebのCSSの一部が動かず、テンプレートの管理が煩雑になりやすい。React Emailはこの問題をJSXで解決する。ResendとReact Emailは同じ開発者(Zeno Rocha)が作っており、テンプレート生成と配信が【一体設計】になっている。普通はテンプレートエンジンと送信サービスは別会社だ。この2つは設計思想が同一であるため、react: <Component />という1行でtemplate → deliveryが完結する。型安全なテンプレート管理とHTTP API送信の組み合わせは、AIにコンテキストを渡す際も単純な関数呼び出しとして表現できる。

選定理由

独立選択の根拠

ResendがCloudflareネイティブ層の一角を担う積極的な理由は、選定軸の4条件を個別に満たしているからだ。

Edge Runtimeとの整合は構造的に保証されている。ResendのSDKは内部でFetch APIを使用しており、Node.js固有のAPIに依存していない。「Edge Runtimeで動くか?」という確認作業が不要で、セットアップ後すぐに動く。

外部SaaS依存の最小化にも寄与する。Resendは単なるHTTPエンドポイントであり、インフラに組み込まれない。Stripeのようにサービスの存続に直結する依存ではなく、同等のHTTP APIを持つサービスへの切り替えコストが低い。

APIキーの発行からメール送信まで数分で完結する設計になっており、IPウォームアップやSPF/DKIMの初期設定も管理画面から直感的に行えます。

なぜ代替案を選ばなかったか

技術 不採用理由
Clerk 認証SaaSとして認証メールはClerk内部で処理するためResendは不要になる。ただしユーザーDB・セッション・認証フローがすべてClerk側に置かれ、データ主権が外部に移る。自前Authとは思想が異なる。
SendGrid 機能が豊富すぎてトランザクショナルメール用途には過剰。バウンス管理・Webhook・IPウォームアップなど運用コストが高い。
nodemailer Node.js依存のため、Cloudflare Workers(Edge Runtime)では動作しない。ライブラリ依存が実行環境制約と矛盾する。
Amazon SES AWSアカウントとIAMポリシーの管理コストが増加する。Cloudflareと異なるベンダーを追加することで外部依存が分散する。
Mailgun APIは類似するが、React Emailとの公式統合がない。テンプレート管理を別途設計する必要が生じる。

構造的メリット

  1. Edge Runtime完全対応のHTTP APIによる呼び出し:ResendのSDKはFetch APIを内部で使用しており、Node.js固有のAPIに依存していません。Cloudflare Workersから直接インポートして使えるため、Edge Runtime対応の確認作業が不要です。

  2. React Emailによる型安全なテンプレート管理:メールのHTMLをJSXで記述できるため、テンプレートのリファクタリング時にTypeScriptの型チェックが機能します。HTMLメールの文字列管理で起きやすい「タグの閉じ忘れ」「変数の埋め込みミス」がコンパイル時に検出されます。

  3. シンプルな料金体系と運用コスト:Resendは1日100通・月3000通まで無料です。小規模SaaSの認証メール用途なら数千ユーザー規模まで無料枠で十分に運用できます。バウンス管理やSPF/DKIMの設定もダッシュボードで完結するため、インフラ管理の負担が最小化されます。

ClaudeMixでの活用例

以下はResendとReact EmailをCloudflare Workersから呼び出すボイラープレートです。

// app/data-io/account/email/sendVerificationEmail.server.ts
import { Resend } from "resend";
import { VerificationEmailTemplate } from "~/components/email/VerificationEmail";

// Resendクライアントの初期化(APIキーはCloudflare Secretで管理)
function getResendClient(apiKey: string): Resend {
  return new Resend(apiKey);
}

export type SendVerificationEmailResult =
  | { success: true }
  | { success: false; error: string };

// メール認証用のトークンをメール送信する
export async function sendVerificationEmail(
  apiKey: string,
  to: string,
  verificationToken: string
): Promise<SendVerificationEmailResult> {
  const resend = getResendClient(apiKey);

  const verificationUrl = `https://example.com/account/verify?token=${verificationToken}`;

  const { error } = await resend.emails.send({
    from: "noreply@example.com",
    to,
    subject: "メールアドレスの確認",
    react: VerificationEmailTemplate({ verificationUrl }),
  });

  if (error) {
    return { success: false, error: error.message };
  }

  return { success: true };
}
// app/components/email/VerificationEmail.tsx(React Email テンプレート)
import {
  Html,
  Head,
  Body,
  Container,
  Text,
  Link,
  Preview,
} from "@react-email/components";

type Props = {
  verificationUrl: string;
};

// React EmailでHTMLメールをJSXとして型安全に定義する
export function VerificationEmailTemplate({ verificationUrl }: Props) {
  return (
    <Html>
      <Head />
      <Preview>メールアドレスの確認</Preview>
      <Body style={{ fontFamily: "sans-serif" }}>
        <Container>
          <Text>以下のリンクをクリックして、メールアドレスを確認してください。</Text>
          <Link href={verificationUrl}>メールアドレスを確認する</Link>
          <Text>このリンクは24時間有効です。</Text>
        </Container>
      </Body>
    </Html>
  );
}
// app/routes/account/register.tsx(React Router ActionでのResend呼び出し例)
import type { ActionFunctionArgs } from "@remix-run/cloudflare";
import type { Env } from "~/types/cloudflare";
import { sendVerificationEmail } from "~/data-io/account/email/sendVerificationEmail.server";
import { generateToken } from "~/lib/account/auth/token.server";

export async function action({ request, context }: ActionFunctionArgs) {
  const env = context.cloudflare.env as Env;
  const formData = await request.formData();
  const email = String(formData.get("email"));

  // 認証トークンを生成してD1に保存する(別途実装)
  const token = await generateToken(env.DB, email);

  // ResendでメールをCloudflare Secret経由で送信する
  const result = await sendVerificationEmail(env.RESEND_API_KEY, email, token);

  if (!result.success) {
    return { error: "メール送信に失敗しました。しばらくお待ちください。" };
  }

  return { message: "確認メールを送信しました。メールをご確認ください。" };
}

エラーハンドリングの思想として、メール送信の失敗はユーザーに対して「しばらくお待ちください」と汎用的に返します。Resendのエラーレスポンスには詳細なエラーコードが含まれますが、ユーザーに送信先の問題(メールアドレス不正等)を詳しく伝えすぎることは、不正なアカウント調査(User Enumeration)につながるリスクがあります。ログには詳細を残しつつ、ユーザーへの表示は最小化する設計が適切です。

公式リファレンス・更新履歴

メール基盤が整い、ユーザー登録・セッション管理・メール送信がCloudflare上で完結した。次に向き合うのは収益化だ。決済基盤はどこに置くか——外部SaaSへの依存をどう設計するかという問いが、Stripeの選定につながる。

あなたのリポジトリへの適用

どちらを採用しますか?(1つ選択)

SIMPLICITY|シンプルに管理

ResendのHTTP APIのみを使用し不要なSDK依存が混入していないか確認する

調査:

  1. Resendとの通信がHTTP API(fetch)のみで実装されており、NodeJS依存のSDKが使われていないか確認せよ
  2. Cloudflare Workers環境でResendの実装が動作するか確認せよ
  3. React Emailのテンプレートがエッジ環境で正しくレンダリングされているか確認せよ

この内容をClaude Codeに貼り付けて実行します

SAFETY|安全性を高める

メール認証フローで送信失敗時のエラーハンドリングが適切か確認する

調査:

  1. Resendへのリクエスト失敗時のエラーハンドリングが実装されているか確認せよ
  2. メール送信失敗がユーザーに適切なエラーメッセージとして伝わるか確認せよ
  3. レート制限やタイムアウトに対する再試行ロジックが必要か確認せよ

この内容をClaude Codeに貼り付けて実行します

外部コードのローカル実行にはリスクがあります。ブラウザ環境での実行を推奨します。