Transaktions-E-Mails mit Claude Code einbauen (Resend + React Email)
So baust du Willkommens-, Beleg- und Passwort-Reset-E-Mails in einer Next.js-16-App mit Resend und React Email, wobei Claude Code die Templates und die Send-Logik schreibt.
Sie möchten das Framework hinter diesen Projekten?
Holen Sie sich das Claude Code System, mit dem wir produktionsreife Software planen, bauen, testen und ausliefern.
Jedes SaaS braucht am ersten Tag drei E-Mails: Willkommen, Beleg, Passwort-Reset. Sie falsch zu machen (im Spam zu landen, einen DKIM-Record zu vergessen, bei einem Tippfehler still zu scheitern) kostet dich Nutzer, bevor die überhaupt das Onboarding beenden. Das ist eine vollständige Anleitung, um Resend und React Email in einer Next.js-16-App mit Claude Code zu verkabeln, von der Domain-Verifizierung bis zu einem funktionierenden Send in einer Server Action.
Warum Transaktions-E-Mail knifflig ist
Transaktions-E-Mail sieht simpel aus, bis du sie tatsächlich auslieferst. Du brauchst eine verifizierte Absender-Domain, sonst gehen deine E-Mails in den Spam oder werden gar nicht verschickt. Du brauchst HTML, das konsistent über Gmail, Outlook und Apple Mail rendert, die wild unterschiedlichen CSS-Support haben. Du musst den Send im richtigen Moment auslösen (nachdem der Signup abgeschlossen ist, nachdem eine Zahlung erfolgreich war), ohne die Response zu blockieren, auf die der Nutzer wartet. Und du musst wissen, wann ein Send tatsächlich fehlgeschlagen ist, statt anzunehmen, dass er geklappt hat.
Resend kümmert sich um Zustellung und Domain-Reputation. React Email kümmert sich um die Templates, sodass du JSX schreibst statt handgetippter Tabellen-Layouts. Claude Code ist hier nützlich, weil es beide APIs schon gut kennt und die Verkabelung (Server Actions, Error-Handling, Environment-Variablen) im ersten Anlauf korrekt hinbekommt, solange du ihm genau sagst, was es bauen soll.
Resend und React Email installieren
Du brauchst drei Pakete: das Resend-SDK zum Versenden, die React-Email-Komponenten-Library zum Bauen der Templates und die React-Email-CLI, um sie lokal zu previewen. Lass Claude Code alle drei installieren, und es nutzt automatisch den Package-Manager deines Projekts.
npm install resend @react-email/components
npm install -D react-emailresend ist der Client, der mit der Resend-API spricht. @react-email/components gibt dir vorgefertigte, E-Mail-sichere Primitives (Html, Body, Container, Button, Text), die korrekt über Mail-Clients rendern, ohne dass du mit Inline-CSS kämpfst. react-email ist eine reine Dev-Dependency, sie treibt den lokalen Preview-Server an, den du später nutzt.
Einen API-Key holen und deine Absender-Domain verifizieren
Registrier dich bei resend.com und erstell einen API-Key über das Dashboard. Speicher ihn als Environment-Variable, hardcode ihn nie in ein Template und committe ihn nie.
# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Your App <hello@yourdomain.com>"Ohne eine verifizierte Domain lässt Resend dich nur an deine eigene Account-E-Mail senden, über die geteilte onboarding@resend.dev-Adresse. Das reicht für einen ersten Test, aber echte Nutzer bekommen darüber nichts. Um an tatsächliche Postfächer zu senden, geh im Resend-Dashboard zu Domains, füg deine Domain hinzu und trag die SPF- und DKIM-Records, die es dir gibt, bei deinem DNS-Provider ein. Die Verifizierung dauert meist ein paar Minuten, sobald die Records propagiert sind, je nach Registrar gelegentlich länger.
Sag Claude Code, welche Domain du nutzt, und es erinnert dich daran, den Verifizierungsstatus zu prüfen, bevor du einen echten Send testest:
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"Ein React-Email-Template bauen
Templates leben in ihrem eigenen Ordner, damit der Preview-Server sie findet und damit sie von den UI-Komponenten deiner App getrennt bleiben. Jedes Template ist eine schlichte React-Komponente, gebaut aus @react-email/components-Primitives, die unter der Haube auf E-Mail-sicheres HTML abbilden.
// 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>
);
}Inline-Styles sind hier Absicht, keine Abkürzung. Die meisten Mail-Clients strippen <style>-Blöcke oder klassenbasiertes CSS, also rendert @react-email/components deine Styles automatisch inline ins finale HTML. Du schreibst normal aussehendes JSX, und es kommt E-Mail-sicher heraus.
Ein lib/email-Helper
Jede Route, die Mail versendet, braucht denselben Resend-Client, also gehört er in einen geteilten Helper, statt überall neu instanziiert zu werden. Das gibt dir auch einen Ort, um später Logging oder einen Fallback zu ergänzen.
// 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 };
}Das Resend-SDK wirft bei einem fehlgeschlagenen Send keine Exception, es gibt ein error-Feld auf dem Response-Objekt zurück. Wenn du diesen Check überspringst, sieht eine gebouncte oder abgelehnte E-Mail in deinen Logs genau wie eine erfolgreiche aus. Der Helper oben zwingt jeden Aufrufer, beide Fälle explizit zu behandeln.
Aus einer Server Action senden (Willkommens-E-Mail nach dem Signup)
Die Willkommens-E-Mail sollte direkt nach der Account-Erstellung feuern, ohne den Nutzer auf die Response des E-Mail-Providers warten zu lassen, bevor er in der App landet. Pack den Send in die Server Action, die nach dem abgeschlossenen Signup läuft.
// 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");
}Eine fehlgeschlagene Willkommens-E-Mail sollte niemals die Account-Erstellung blockieren. Der Nutzer hat an dem Punkt schon einen Account, also loggt der Code den Fehler und macht mit dem Dashboard weiter, statt eine Exception zu werfen. Diese Log-Zeile zählt, lass Claude Code sie in deinen bestehenden Error-Tracker einbinden, falls du einen hast, damit ein Anstieg an E-Mail-Fehlern tatsächlich bemerkt wird, statt still in den Server-Logs zu liegen.
Aus einem Route Handler senden (Passwort-Reset)
Ein Passwort-Reset braucht ein Token, das vor dem Send generiert wird, und wird oft von einem Formular ausgelöst, das eine JSON-Response statt eines Redirects erwartet, also passt ein Route Handler hier besser als eine Server Action.
// 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 });
}Dieselbe Erfolgs-Response zurückzugeben, egal ob der Account existiert oder nicht, ist ein kleines, aber echtes Sicherheitsdetail. Frag Claude Code explizit nach diesem Pattern ("don't leak account existence through the response"), weil eine naive Implementierung einen 404 für unbekannte E-Mails zurückgibt, was genau die Information ist, die du nicht herausgeben willst.
Die Beleg-E-Mail bauen
Belege folgen demselben Pattern wie die Willkommens-E-Mail, nur mit anderem Inhalt und, meistens, einem Stripe-Webhook als Auslöser statt eines Formular-Submits.
// 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>
);
}Ruf das aus deinem Stripe-Webhook-Handler auf, nach einem checkout.session.completed- oder invoice.paid-Event, mit demselben sendEmail-Helper von vorhin. Stripe versucht fehlgeschlagene Webhooks erneut, also wenn dein E-Mail-Send innerhalb dieses Handlers eine Exception wirft, wrappe ihn, damit ein schlechter Send Stripe nicht denken lässt, der ganze Webhook sei fehlgeschlagen, und das gesamte Event erneut auslöst.
Templates lokal previewen
Du willst nicht bei jeder Anpassung einer Schriftgröße eine echte E-Mail verschicken. Die React-Email-CLI startet einen lokalen Server, der deine Templates live rendert und beim Speichern neu lädt, ohne API-Key oder Netzwerk-Call.
npx react-email devDas öffnet einen Preview auf localhost:3000 (oder dem nächsten freien Port), der jede Komponente in deinem emails/-Ordner zeigt, gerendert, wie sie in einem Postfach erscheinen würde, mit einem Umschalter, um das rohe HTML-Output zu inspizieren. Zeig mit --dir auf einen eigenen Ordner, falls deine Templates nicht am Standardort liegen. Lass Claude Code Mock-Props für jedes Template übergeben, damit der Preview echt aussehenden Inhalt statt leerer Felder zeigt.
Fehler und Idempotenz behandeln
Zwei Fehlermodi zählen in Produktion. Erstens kann ein langsamer oder fehlgeschlagener Call an Resend innerhalb einer Server Action einen Request hängen lassen, wenn du keine vernünftige Erwartung setzt, wie lange du wartest. Zweitens können Retries (ein Nutzer, der doppelt auf Submit klickt, ein Webhook, der zweimal feuert) Doppel-Sends auslösen, wenn nichts sie stoppt.
Zum Doppel-Schutz übergib einen Idempotency-Key bei Sends, die von externen Events wie Webhooks ausgelöst werden, damit ein wiederholter Webhook nicht denselben Beleg zweimal schickt.
await resend.emails.send(
{
from: process.env.EMAIL_FROM!,
to: user.email,
subject: "Payment received",
react: ReceiptEmail({ amount, planName, invoiceDate }),
},
{ idempotencyKey: `receipt-${invoiceId}` }
);Die Stripe-Invoice-ID als Idempotency-Key zu nutzen bedeutet, dass Resend einen wiederholten Request mit demselben Key erkennt und das erneute Senden überspringt, statt einen zweiten Beleg zu feuern. Das zählt am meisten bei webhook-ausgelösten Sends, wo Retries erwartetes Verhalten sind, kein Edge Case.
Testen, bevor du auslieferst
Bevor du das in Signup oder Checkout einbaust, schick eine echte Test-E-Mail an dein eigenes Postfach und prüf drei Dinge von Hand: dass die Betreffzeile nicht abgeschnitten ist, dass der Button tatsächlich auf die richtige URL verlinkt und dass die E-Mail nicht im Spam landet. Gmail und Outlook behandeln Inline-Styles unterschiedlich, also prüf beide, wenn du kannst.
claude "send a test welcome email to my inbox using the sendEmail helper and confirm the response includes a data.id"Sobald das klappt, sieh im Logs-Tab des Resend-Dashboards nach. Jeder Send zeigt seinen Zustellstatus (delivered, bounced, complained), was der schnellste Weg ist, um zu bestätigen, dass deine Domain-Verifizierung tatsächlich von Anfang bis Ende funktioniert, nicht nur von der API akzeptiert wurde.
E-Mail von Hand zu verkabeln, ein Endpoint nach dem anderen, ist die Art Aufgabe, die Claude Code in einer einzigen Session gut bewältigt. Der schwierigere Teil ist alles drumherum: die Domain korrekt verifizieren, sicherstellen, dass ein fehlgeschlagener Send nicht still den Signup kaputtmacht, und die Templates konsistent halten, während du mehr davon hinzufügst. Das $29 Code Kit liefert das schon verkabelt in das Framework, das es aufsetzt, Willkommens-, Beleg- und Reset-Templates inklusive, damit du von einem funktionierenden E-Mail-System startest, statt eins von Grund auf zu bauen.
Posted by @speedy_devv
Sie möchten das Framework hinter diesen Projekten?
Holen Sie sich das Claude Code System, mit dem wir produktionsreife Software planen, bauen, testen und ausliefern.
Authentifizierung mit Claude Code einbauen (Supabase Auth)
Ergänze E-Mail/Passwort-Signup, Google OAuth, Magic Links, geschützte Routen und Session-Handling in einer Next.js-16-App mit Claude Code und Supabase Auth.
Eine typsichere API mit Claude Code bauen (oRPC + Zod)
So baust du eine vollständig typsichere API-Schicht in Next.js 16 mit oRPC und Zod, mit Claude Code, sodass Schema-Änderungen den Build brechen statt der Produktion.

