Ajouter les emails transactionnels avec Claude Code (Resend + React Email)
Comment construire des emails de bienvenue, de reçu et de réinitialisation de mot de passe dans une app Next.js 16 avec Resend et React Email, en laissant Claude Code écrire les templates et la logique d'envoi.
Vous voulez le framework derrière ces projets ?
Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.
Chaque SaaS a besoin de trois emails dès le premier jour : bienvenue, reçu, réinitialisation de mot de passe. Les rater (atterrir dans les spams, manquer un enregistrement DKIM, échouer en silence sur une faute de frappe) te coûte des utilisateurs avant même qu'ils aient fini leur onboarding. Voici un guide complet pour brancher Resend et React Email dans une app Next.js 16 avec Claude Code, de la vérification de domaine à un envoi qui marche dans une Server Action.
Pourquoi l'email transactionnel est capricieux
L'email transactionnel a l'air simple jusqu'à ce que tu l'expédies pour de vrai. Il te faut un domaine d'envoi vérifié ou tes emails partent en spam, ou ne partent pas du tout. Il te faut du HTML qui s'affiche de façon cohérente sur Gmail, Outlook et Apple Mail, qui ont un support CSS radicalement différent. Il faut déclencher l'envoi au bon moment (après la fin de l'inscription, après un paiement réussi) sans bloquer la réponse que l'utilisateur attend. Et il faut savoir quand un envoi a vraiment échoué au lieu de supposer qu'il a marché.
Resend gère la livraison et la réputation du domaine. React Email gère les templates, pour que tu écrives du JSX au lieu de mises en page à base de tableaux codées à la main. Claude Code est utile ici parce qu'il connaît déjà bien les deux API et peut brancher la plomberie (Server Actions, gestion d'erreurs, variables d'environnement) correctement au premier passage, tant que tu lui dis exactement quoi construire.
Installer Resend et React Email
Il te faut trois packages : le SDK Resend pour envoyer les mails, la bibliothèque de composants React Email pour construire les templates, et la CLI React Email pour les prévisualiser en local. Demande à Claude Code de les installer tous les trois et il utilisera automatiquement le gestionnaire de packages de ton projet.
npm install resend @react-email/components
npm install -D react-emailresend est le client qui parle à l'API Resend. @react-email/components te donne des primitives pré-construites et compatibles email (Html, Body, Container, Button, Text) qui s'affichent correctement sur les clients mail sans que tu te battes avec du CSS inline. react-email est une dépendance de dev uniquement, elle alimente le serveur de preview local que tu utiliseras plus tard.
Obtenir une clé API et vérifier ton domaine d'envoi
Inscris-toi sur resend.com et crée une clé API depuis le dashboard. Stocke-la dans une variable d'environnement, ne la mets jamais en dur dans un template et ne la commite jamais.
# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Your App <hello@yourdomain.com>"Sans domaine vérifié, Resend ne te laisse envoyer qu'à l'email de ton propre compte en utilisant l'adresse partagée onboarding@resend.dev. C'est bien pour un premier test, mais de vrais utilisateurs ne recevront rien de sa part. Pour envoyer vers de vraies boîtes de réception, va dans Domains dans le dashboard Resend, ajoute ton domaine, et ajoute les enregistrements SPF et DKIM qu'il te donne chez ton fournisseur DNS. La vérification prend en général quelques minutes une fois les enregistrements propagés, parfois plus selon ton registrar.
Dis à Claude Code quel domaine tu utilises et il te rappellera de vérifier le statut de vérification avant de tester un vrai envoi :
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"Construire un template React Email
Les templates vivent dans leur propre dossier pour que le serveur de preview les trouve et pour qu'ils restent séparés des composants UI de ton app. Chaque template est un simple composant React construit à partir des primitives de @react-email/components, qui se traduisent en HTML compatible email en coulisses.
// 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>
);
}Les styles inline sont intentionnels ici, pas un raccourci. La plupart des clients mail suppriment les blocs <style> ou le CSS à base de classes, donc @react-email/components rend tes styles inline dans le HTML final automatiquement. Tu écris du JSX qui a l'air normal et il ressort compatible email.
Un helper lib/email
Chaque route qui envoie du mail a besoin du même client Resend, il a donc sa place dans un seul helper partagé plutôt que d'être ré-instancié partout. Ça te donne aussi un seul endroit où ajouter du logging ou un fallback plus tard.
// 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 };
}Le SDK Resend ne lève pas d'exception sur un envoi échoué, il renvoie un champ error sur l'objet de réponse. Si tu sautes cette vérification, un email rejeté ou en bounce ressemble exactement à un envoi réussi dans tes logs. Le helper ci-dessus force chaque appelant à gérer les deux cas explicitement.
Envoyer depuis une Server Action (email de bienvenue après inscription)
L'email de bienvenue devrait partir juste après la création du compte, sans faire attendre l'utilisateur sur la réponse du fournisseur d'email avant qu'il n'atterrisse sur l'app. Enrobe l'envoi dans la Server Action qui tourne une fois l'inscription terminée.
// 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");
}Un email de bienvenue échoué ne devrait jamais bloquer la création du compte. L'utilisateur a déjà un compte à ce stade, donc le code logue l'échec et continue vers le dashboard au lieu de lever une exception. Cette ligne de log compte, demande à Claude Code de la brancher dans ton tracker d'erreurs existant si tu en as un, pour qu'un pic d'échecs d'envoi soit vraiment remarqué au lieu de dormir tranquillement dans les logs serveur.
Envoyer depuis un Route Handler (réinitialisation de mot de passe)
La réinitialisation de mot de passe a besoin d'un token généré en amont de l'envoi, et elle est souvent déclenchée depuis un formulaire qui attend une réponse JSON plutôt qu'une redirection, donc un Route Handler convient mieux qu'une Server Action ici.
// 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 });
}Renvoyer la même réponse de succès que le compte existe ou non est un détail de sécurité petit mais réel. Demande explicitement ce pattern à Claude Code (« ne divulgue pas l'existence d'un compte via la réponse ») parce qu'une implémentation naïve renvoie un 404 pour les emails inconnus, ce qui est exactement l'information que tu ne veux pas distribuer.
Construire l'email de reçu
Les reçus suivent le même pattern que l'email de bienvenue, juste avec un contenu différent et, en général, un webhook Stripe comme déclencheur au lieu d'une soumission de formulaire.
// 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>
);
}Appelle-le depuis ton gestionnaire de webhook Stripe après un événement checkout.session.completed ou invoice.paid, en utilisant le même helper sendEmail que tout à l'heure. Stripe réessaie les webhooks échoués, donc si ton envoi d'email lève une exception dans ce gestionnaire, enrobe-le pour qu'un mauvais envoi ne fasse pas croire à Stripe que tout le webhook a échoué et ne le pousse pas à rejouer l'événement entier.
Prévisualiser les templates en local
Tu ne veux pas envoyer un vrai email à chaque fois que tu ajustes une taille de police. La CLI React Email démarre un serveur local qui rend tes templates en direct et recharge à la sauvegarde, sans clé API ni appel réseau.
npx react-email devÇa ouvre une preview sur localhost:3000 (ou le prochain port libre) montrant chaque composant de ton dossier emails/, rendu comme il apparaîtrait dans une boîte de réception, avec un bouton pour inspecter le HTML brut. Pointe-le vers un dossier personnalisé avec --dir si tes templates ne vivent pas à l'emplacement par défaut. Demande à Claude Code de passer des props factices pour chaque template pour que la preview montre un contenu qui a l'air réel au lieu de champs vides.
Gérer les erreurs et l'idempotence
Deux modes d'échec comptent en production. D'abord, un appel lent ou échoué à Resend dans une Server Action peut laisser une requête suspendue si tu ne fixes pas une attente raisonnable sur le temps que tu vas patienter. Ensuite, les réessais (un utilisateur qui double-clique sur envoyer, un webhook qui part deux fois) peuvent déclencher des envois en double si rien ne les arrête.
Pour la protection contre les doublons, passe une clé d'idempotence sur les envois déclenchés par des événements externes comme les webhooks, pour qu'un webhook rejoué n'envoie pas deux fois le même reçu.
await resend.emails.send(
{
from: process.env.EMAIL_FROM!,
to: user.email,
subject: "Payment received",
react: ReceiptEmail({ amount, planName, invoiceDate }),
},
{ idempotencyKey: `receipt-${invoiceId}` }
);Utiliser l'ID de facture Stripe comme clé d'idempotence veut dire que Resend reconnaît une requête rejouée avec la même clé et saute son envoi, au lieu de tirer un second reçu. Ça compte surtout sur les envois déclenchés par webhook, où les réessais sont un comportement attendu, pas un cas limite.
Tester avant d'expédier
Avant de brancher ça dans l'inscription ou le checkout, envoie un vrai email de test à ta propre boîte de réception et vérifie trois choses à la main : l'objet n'est pas tronqué, le bouton pointe vraiment vers la bonne URL, et l'email n'atterrit pas en spam. Gmail et Outlook gèrent les styles inline différemment, alors vérifie les deux si tu peux.
claude "send a test welcome email to my inbox using the sendEmail helper and confirm the response includes a data.id"Une fois que ça marche, va voir l'onglet Logs du dashboard Resend. Chaque envoi affiche son statut de livraison (delivered, bounced, complained), ce qui est le moyen le plus rapide de confirmer que la vérification de ton domaine marche vraiment de bout en bout, pas juste acceptée par l'API.
Brancher l'email à la main, un endpoint à la fois, c'est le genre de tâche que Claude Code gère bien en une seule session. Le plus dur, c'est tout ce qu'il y a autour : faire vérifier le domaine correctement, s'assurer qu'un envoi échoué ne casse pas l'inscription en silence, et garder les templates cohérents à mesure que tu en ajoutes. Le Code Kit à $29 livre ça déjà branché dans le framework qu'il met en place, templates de bienvenue, de reçu et de réinitialisation inclus, pour que tu partes d'un système d'email qui marche au lieu d'en construire un de zéro.
Posté par @speedy_devv
Vous voulez le framework derrière ces projets ?
Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.
Ajouter l'authentification avec Claude Code (Supabase Auth)
Ajoute l'inscription email/mot de passe, l'OAuth Google, les magic links, les routes protégées et la gestion des sessions à une app Next.js 16 avec Claude Code et Supabase Auth.
Construire une API type-safe avec Claude Code (oRPC + Zod)
Comment construire une couche API entièrement type-safe dans Next.js 16 avec oRPC et Zod, avec Claude Code, pour que les changements de schéma cassent le build plutôt que la prod.

