Build This Now
Build This Now
クロード・コードとは何か?Claude Code のインストールClaude Code ネイティブインストーラーClaude Code で最初のプロジェクトを作る
Failed Payment RecoveryLocalization and i18nPDF InvoicesReferral ProgramTwo-Factor AuthNext.js DevTools MCPNext.js Agent SetupSubscription BillingMulti-Tenant SaaSAI Chat FeatureRate LimitingStripe WebhooksSemantic SearchRealtime UpdatesDrip Email Sequencesv2.1.122 Release NotesClaude Code の Dynamic Workflows:実際のコードベースで 1,000 個の subagents を動かす方法Claude Code ベストプラクティスClaude Opus 4.7 ベストプラクティスVPS上でのClaude CodeGit 統合Claude Code レビューClaude Code WorktreesClaude CodeリモートコントロールClaude Code ChannelsChannels、Routines、Teleport、DispatchClaude Code スケジュールタスクClaude Code権限管理Claude Code オートモードClaude Code で Stripe 決済を組み込むフィードバックループTodoワークフローClaude Code タスク管理プロジェクトテンプレートClaude Code の料金とトークン使用量Claude Code の料金:実際にいくら払うことになるのかClaude Code Ultra Review 完全ガイドClaude Code で Next.js アプリを作るSupabase DatabaseVercel DeepsecTest-Driven DevelopmentClaude Code で SaaS の MVP を作る方法Claude Code で認証を追加する(Supabase Auth)Claude Code でトランザクションメールを追加する(Resend + React Email)Claude Code で型安全な API を作る(oRPC + Zod)File UploadsBackground Jobs (Inngest)Admin DashboardFull-Text SearchClaude Agent SDKKiro Migration GuideClaude Research AgentLeave Grok BuildRoute Subagent ModelsClaude Monorepo SetupCI Repair AgentVisual Regression TestsAgent Cost DashboardCursor Migration Guideエージェント型コマース:AI エージェントが支払えるアプリの作り方1M ContextUser API KeysAudit LogsCSV Import PipelineDatabase MigrationsProduction Error TrackingFeature FlagsGitHub ActionsHeadless ModeMax Plan vs APICaching and RevalidationIn-App NotificationsOutbound WebhooksPrompt CachingRoles & PermissionsMarketplace PaymentsUsage-Based Billing2026年、Claude Code で SaaS を作るといくらかかるかParallel AI AgentsCoding Agent Injection
speedy_devvkoen_salo
Blog/Handbook/Workflow/Transactional Email

Claude Code でトランザクションメールを追加する(Resend + React Email)

Resend と React Email を使って、Next.js 16 アプリでウェルカム・レシート・パスワードリセットのメールを作る方法。テンプレートと送信ロジックは Claude Code が書きます。

設定をやめて、構築を始めよう。

AIオーケストレーション付きSaaSビルダーテンプレート。

企業向けに構築している実績を見る →
speedy_devvkoen_salo
speedy_devvWritten by speedy_devvPublished Jul 14, 20269 min readHandbook hubWorkflow index

どんな SaaS も初日に 3 通のメールが要ります。ウェルカム、レシート、パスワードリセットです。これを間違えると(スパム行き、DKIM レコードの欠落、タイプミスで黙って失敗)、ユーザーはオンボーディングを終える前に離れていきます。これは、Next.js 16 アプリで Resend と React Email を繋ぐ完全な手順です。ドメインの検証から、Server Action での動く送信までをカバーします。

トランザクションメールが厄介な理由

トランザクションメールは、実際に出荷するまでは単純に見えます。検証済みの送信ドメインがないとメールはスパムに行くか、そもそも送れません。Gmail、Outlook、Apple Mail は CSS のサポートがバラバラなので、それらすべてで一貫してレンダリングされる HTML が要ります。ユーザーが待っているレスポンスをブロックせずに、適切なタイミング(サインアップ完了後、決済成功後)で送信をトリガーする必要があります。そして、うまくいったと思い込むのではなく、送信が実際に失敗したのを知る必要があります。

Resend は配信とドメインの評判を扱います。React Email はテンプレートを扱うので、手書きのテーブルレイアウトではなく JSX を書けます。ここで Claude Code が役立つのは、両方の API をすでによく知っていて、配管(Server Actions、エラー処理、環境変数)を最初のパスで正しく繋げるからです。何を作るかを正確に伝える限りは。

Resend と React Email をインストールする

パッケージは 3 つ必要です。メールを送る Resend SDK、テンプレートを作る React Email のコンポーネントライブラリ、そしてローカルでプレビューする React Email CLI です。3 つすべてをインストールするよう Claude Code に頼めば、プロジェクトのパッケージマネージャを自動で使ってくれます。

npm install resend @react-email/components
npm install -D react-email

resend は Resend API と話すクライアントです。@react-email/components は、インライン CSS と格闘せずにメールクライアント全体で正しくレンダリングされる、あらかじめ用意されたメールセーフなプリミティブ(Html、Body、Container、Button、Text)を提供します。react-email は開発時だけの依存で、あとで使うローカルプレビューサーバーを動かします。

API キーを取得し、送信ドメインを検証する

resend.com でサインアップし、ダッシュボードから API キーを作ります。これは環境変数として保存し、テンプレートにハードコードしたりコミットしたりは絶対にしないでください。

# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Your App <hello@yourdomain.com>"

検証済みのドメインがないと、Resend は共有の onboarding@resend.dev アドレスを使って自分のアカウントのメールにしか送らせてくれません。最初のテストには十分ですが、実際のユーザーはそこから何も受け取れません。本物の受信箱に送るには、Resend ダッシュボードの Domains に行き、ドメインを追加して、提示される SPF と DKIM のレコードを DNS プロバイダーに追加します。検証はレコードが伝播すればたいてい数分で済みますが、レジストラによってはもう少しかかることもあります。

どのドメインを使うかを Claude Code に伝えておけば、本物の送信をテストする前に検証状況を確認するよう促してくれます。

claude "add a startup check that warns in the console if EMAIL_FROM's domain isn't verified in Resend, using the domains.list API"

React Email のテンプレートを作る

テンプレートは、プレビューサーバーが見つけられるように、そしてアプリの UI コンポーネントと分けておくために、専用のフォルダに置きます。各テンプレートは @react-email/components のプリミティブから作られたただの React コンポーネントで、内部でメールセーフな HTML にマッピングされます。

// emails/welcome-email.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";

interface WelcomeEmailProps {
  name: string;
  dashboardUrl: string;
}

export default function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Your account is ready</Preview>
      <Body style={{ backgroundColor: "#f6f6f6", fontFamily: "sans-serif" }}>
        <Container style={{ backgroundColor: "#ffffff", padding: "32px", borderRadius: "8px" }}>
          <Heading style={{ fontSize: "20px" }}>Welcome, {name}</Heading>
          <Text style={{ color: "#444", fontSize: "15px", lineHeight: "22px" }}>
            Your account is set up and ready to go. Click below to get started.
          </Text>
          <Section style={{ marginTop: "24px" }}>
            <Button
              href={dashboardUrl}
              style={{
                backgroundColor: "#111",
                color: "#fff",
                padding: "12px 20px",
                borderRadius: "6px",
                fontSize: "14px",
              }}
            >
              Go to dashboard
            </Button>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

ここでインラインスタイルにしているのは意図的で、手抜きではありません。ほとんどのメールクライアントは <style> ブロックやクラスベースの CSS を剥ぎ取るので、@react-email/components はスタイルを自動的に最終 HTML にインライン展開します。あなたは普通に見える JSX を書くだけで、メールセーフになって出てきます。

lib/email ヘルパー

メールを送るすべてのルートは同じ Resend クライアントを必要とするので、あちこちで作り直すのではなく、1 つの共有ヘルパーに置くのが筋です。こうすると、あとでログやフォールバックを追加する場所も 1 か所にまとまります。

// lib/email.ts
import { Resend } from "resend";
import type { ReactElement } from "react";

const resend = new Resend(process.env.RESEND_API_KEY);

interface SendEmailParams {
  to: string;
  subject: string;
  react: ReactElement;
}

export async function sendEmail({ to, subject, react }: SendEmailParams) {
  const { data, error } = await resend.emails.send({
    from: process.env.EMAIL_FROM!,
    to,
    subject,
    react,
  });

  if (error) {
    console.error("Email send failed:", error);
    return { success: false as const, error };
  }

  return { success: true as const, id: data?.id };
}

Resend SDK は送信失敗時に例外を投げず、レスポンスオブジェクトに error フィールドを返します。そのチェックを飛ばすと、バウンスや拒否されたメールが、ログ上では成功とまったく同じに見えてしまいます。上のヘルパーは、すべての呼び出し元に両方のケースを明示的に扱わせます。

Server Action から送る(サインアップ後のウェルカムメール)

ウェルカムメールは、ユーザーがアプリに着地する前にメールプロバイダーのレスポンスを待たせることなく、アカウント作成の直後に飛ばすべきです。送信を、サインアップ完了後に走る Server Action の中に包みます。

// app/(auth)/signup/actions.ts
"use server";

import { createUser } from "@/lib/db";
import { sendEmail } from "@/lib/email";
import WelcomeEmail from "@/emails/welcome-email";
import { redirect } from "next/navigation";

export async function signUp(formData: FormData) {
  const email = formData.get("email") as string;
  const name = formData.get("name") as string;
  const password = formData.get("password") as string;

  const user = await createUser({ email, name, password });

  const result = await sendEmail({
    to: user.email,
    subject: "Welcome aboard",
    react: WelcomeEmail({
      name: user.name,
      dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
    }),
  });

  if (!result.success) {
    console.error(`Welcome email failed for user ${user.id}`, result.error);
  }

  redirect("/dashboard");
}

ウェルカムメールの失敗が、アカウント作成をブロックすることは決してあってはいけません。その時点でユーザーはすでにアカウントを持っているので、このコードは例外を投げるのではなく失敗をログに残してダッシュボードへ進みます。そのログ行が大事です。既存のエラートラッカーがあるなら、そこに繋ぎ込むよう Claude Code に頼みましょう。そうすればメール失敗の急増が、サーバーログに静かに埋もれるのではなく、実際に気づかれます。

Route Handler から送る(パスワードリセット)

パスワードリセットは送信の前にトークンを生成する必要があり、リダイレクトではなく JSON レスポンスを期待するフォームからトリガーされることが多いので、ここでは Server Action よりも Route Handler が合います。

// app/api/auth/reset-password/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createResetToken, getUserByEmail } from "@/lib/db";
import { sendEmail } from "@/lib/email";
import ResetPasswordEmail from "@/emails/reset-password-email";

export async function POST(request: NextRequest) {
  const { email } = await request.json();
  const user = await getUserByEmail(email);

  // Always return success, even if the user doesn't exist.
  // This avoids leaking which emails have accounts.
  if (!user) {
    return NextResponse.json({ success: true });
  }

  const token = await createResetToken(user.id);
  const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password/${token}`;

  const result = await sendEmail({
    to: user.email,
    subject: "Reset your password",
    react: ResetPasswordEmail({ resetUrl }),
  });

  if (!result.success) {
    return NextResponse.json({ success: false, error: "Failed to send email" }, { status: 500 });
  }

  return NextResponse.json({ success: true });
}

アカウントが存在するかどうかにかかわらず同じ成功レスポンスを返すのは、小さいけれど本物のセキュリティ上の配慮です。このパターンは Claude Code に明示的に頼みましょう(「レスポンスからアカウントの有無を漏らさないで」)。素朴な実装は未知のメールに 404 を返しますが、それはまさに渡したくない情報だからです。

レシートメールを作る

レシートはウェルカムメールと同じパターンに従います。違うのは中身と、トリガーがフォーム送信ではなく、たいていは Stripe の webhook である点だけです。

// emails/receipt-email.tsx
import { Body, Container, Head, Heading, Hr, Html, Row, Column, Section, Text } from "@react-email/components";

interface ReceiptEmailProps {
  amount: string;
  planName: string;
  invoiceDate: string;
}

export default function ReceiptEmail({ amount, planName, invoiceDate }: ReceiptEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: "#f6f6f6", fontFamily: "sans-serif" }}>
        <Container style={{ backgroundColor: "#ffffff", padding: "32px", borderRadius: "8px" }}>
          <Heading style={{ fontSize: "20px" }}>Payment received</Heading>
          <Section style={{ marginTop: "16px" }}>
            <Row>
              <Column><Text>Plan</Text></Column>
              <Column><Text>{planName}</Text></Column>
            </Row>
            <Row>
              <Column><Text>Amount</Text></Column>
              <Column><Text>{amount}</Text></Column>
            </Row>
            <Row>
              <Column><Text>Date</Text></Column>
              <Column><Text>{invoiceDate}</Text></Column>
            </Row>
          </Section>
          <Hr />
          <Text style={{ color: "#888", fontSize: "12px" }}>
            Questions about this charge? Reply to this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

これは Stripe の webhook ハンドラの中で、checkout.session.completed や invoice.paid のイベントのあとに、先ほどと同じ sendEmail ヘルパーを使って呼びます。Stripe は失敗した webhook をリトライするので、そのハンドラの中でメール送信が例外を投げるなら、送信の失敗が原因で Stripe が webhook 全体を失敗とみなしてイベント全体をリトライしないよう、送信を包んでおきましょう。

テンプレートをローカルでプレビューする

フォントサイズを微調整するたびに本物のメールを送りたくはありません。React Email CLI はローカルサーバーを立ち上げ、テンプレートをライブでレンダリングして保存のたびにリロードします。API キーもネットワーク呼び出しも要りません。

npx react-email dev

これは localhost:3000(または次に空いているポート)でプレビューを開き、emails/ フォルダのすべてのコンポーネントを受信箱に表示されるとおりにレンダリングし、生の HTML 出力を確認するトグルも付けてくれます。テンプレートがデフォルトの場所にないなら、--dir でカスタムフォルダを指定しましょう。各テンプレートにモックの props を渡すよう Claude Code に頼めば、プレビューが空欄ではなく本物らしい内容を表示します。

エラーとべき等性を扱う

本番では 2 つの失敗モードが効いてきます。1 つ目、Server Action の中での Resend への呼び出しが遅かったり失敗したりすると、待つ時間の妥当な上限を決めておかない限りリクエストが宙ぶらりんになり得ます。2 つ目、リトライ(ユーザーの送信ダブルクリック、webhook の二重発火)は、何も止めなければ重複送信を引き起こし得ます。

重複対策として、webhook のような外部イベントでトリガーされる送信にはべき等キーを渡し、リトライされた webhook が同じレシートを 2 回送らないようにします。

await resend.emails.send(
  {
    from: process.env.EMAIL_FROM!,
    to: user.email,
    subject: "Payment received",
    react: ReceiptEmail({ amount, planName, invoiceDate }),
  },
  { idempotencyKey: `receipt-${invoiceId}` }
);

Stripe のインボイス ID をべき等キーに使うと、Resend は同じキーのリトライされたリクエストを認識して再送をスキップし、2 通目のレシートを飛ばしません。これがいちばん効くのは webhook でトリガーされる送信で、そこではリトライはエッジケースではなく想定された挙動です。

出荷前にテストする

これをサインアップや checkout に繋ぐ前に、自分の受信箱に本物のテストメールを送って、3 つを手で確認しましょう。件名が途切れていないこと、ボタンが実際に正しい URL にリンクしていること、そしてメールがスパムに行かないこと。Gmail と Outlook はインラインスタイルの扱いが違うので、できれば両方確認してください。

claude "send a test welcome email to my inbox using the sendEmail helper and confirm the response includes a data.id"

これが動いたら、Resend ダッシュボードの Logs タブを確認します。すべての送信が配信状況(delivered、bounced、complained)を表示し、これがドメインの検証が API に受理されただけでなく、端から端まで実際に機能していることを確認する最速の方法です。

メールを 1 エンドポイントずつ手で繋ぐのは、Claude Code が 1 セッションでうまくこなす類のタスクです。難しいのはその周りのすべてです。ドメインを正しく検証すること、送信の失敗が黙ってサインアップを壊さないようにすること、そしてテンプレートを増やしても一貫性を保つこと。$29 の Code Kit は、それがセットアップするフレームワークにこれをすでに繋いだ状態で出荷します。ウェルカム・レシート・リセットのテンプレート込みなので、あなたはゼロから作るのではなく、動くメールシステムから始められます。

Posted by @speedy_devv

Continue in Workflow

  • エージェント型コマース:AI エージェントが支払えるアプリの作り方
    2026年のエージェント型コマースをわかりやすく解説するガイド。x402、ACP、Machine Payments Protocol が何をするのか、そして AI エージェントが購入できる有料 API を週末で出荷するための手順を紹介します。
  • Claude Code ベストプラクティス
    Claude Codeで成果を出すエンジニアを分ける5つの習慣: PRD、モジュラーなCLAUDE.mdのルール、カスタムスラッシュコマンド、/clearリセット、そしてシステム進化の思考法。
  • Claude Code オートモード
    2つ目の Sonnet モデルが、Claude Code のすべてのツール呼び出しを実行前に審査します。オートモードがブロックするもの・許可するもの、そして settings.json に追加される許可ルールについて解説します。
  • Channels、Routines、Teleport、Dispatch
    Anthropic が2026年3月と4月に出荷した4つの Claude Code 機能。これらは CLI を、スマホ・ウェブ・デスクトップをまたぐイベント駆動の調整レイヤーに変えます。
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Best SaaS Boilerplate 2026: The Honest Comparison
    An honest 2026 roundup of the best SaaS boilerplates and starter kits (ShipFast, Makerkit, Supastarter, SaaS Pegasus, Divjoy, open source), with real pricing, stacks, and the trade-off nobody mentions: a boilerplate still leaves you coding.
  • Claude Code 変更履歴
    v0.2ベータから2026年3月までのClaude Codeのリリースノート。ベアモード、チャンネル権限リレー、OAuthの修正、すべての破壊的変更を網羅。
  • What It Really Costs to Build a SaaS MVP in 2026
    A buyer's cost breakdown for building a SaaS MVP in 2026. Real freelancer, agency, no-code, AI-tool, and DIY numbers with citations, plus where the money actually goes.
  • コードを生み出すプロンプトテンプレート
    コードを出荷する10のプロンプトレシピ: フルスタックスキャフォールディング、API、スキーマ、テスト、リファクタリング、デバッグ、レビュー、CI。それぞれの失敗パターンも紹介。

設定をやめて、構築を始めよう。

AIオーケストレーション付きSaaSビルダーテンプレート。

企業向けに構築している実績を見る →
speedy_devvkoen_salo

Claude Code で認証を追加する(Supabase Auth)

Claude Code と Supabase Auth を使って、Next.js 16 アプリにメール/パスワードのサインアップ、Google OAuth、magic link、保護されたルート、セッション管理を追加する方法。

Claude Code で型安全な API を作る(oRPC + Zod)

oRPC と Zod を使って、Next.js 16 で完全に型安全な API 層を Claude Code で作る方法。スキーマ変更が本番ではなくビルドを壊すようにする。

On this page

トランザクションメールが厄介な理由
Resend と React Email をインストールする
API キーを取得し、送信ドメインを検証する
React Email のテンプレートを作る
lib/email ヘルパー
Server Action から送る(サインアップ後のウェルカムメール)
Route Handler から送る(パスワードリセット)
レシートメールを作る
テンプレートをローカルでプレビューする
エラーとべき等性を扱う
出荷前にテストする

設定をやめて、構築を始めよう。

AIオーケストレーション付きSaaSビルダーテンプレート。

企業向けに構築している実績を見る →