Build This Now
Build This Now
Qu'est-ce que le code Claude ?Installer Claude CodeL'installateur natif de Claude CodeTon premier projet 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 : comment orchestrer 1 000 sous-agents sur une vraie codebaseBonnes pratiques Claude CodeMeilleures pratiques pour Claude Opus 4.7Claude Code sur un VPSIntégration GitRevue de code avec Claude CodeLes Worktrees avec Claude CodeClaude Code à distanceClaude Code ChannelsChannels, Routines, Teleport, DispatchTâches planifiées avec Claude CodePermissions Claude CodeLe mode auto de Claude CodeAjouter les paiements Stripe avec Claude CodeFeedback LoopsWorkflows TodoGestion des tâches dans Claude CodeTemplates de projetTarification et utilisation des tokens Claude CodeTarifs de Claude Code : ce que tu vas vraiment payerClaude Code Ultra ReviewConstruire une app Next.js avec Claude CodeSupabase DatabaseVercel DeepsecTest-Driven DevelopmentConstruire un MVP SaaS avec Claude CodeAjouter l'authentification avec Claude Code (Supabase Auth)Ajouter les emails transactionnels avec Claude Code (Resend + React Email)Construire une API type-safe avec Claude Code (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 GuideCommerce agentique : comment construire une app que les agents IA peuvent payer1M ContextUser API KeysAudit LogsCSV Import PipelineDatabase MigrationsProduction Error TrackingFeature FlagsGitHub ActionsHeadless ModeMax Plan vs APICaching and RevalidationIn-App NotificationsOutbound WebhooksPrompt CachingRoles & PermissionsMarketplace PaymentsUsage-Based BillingCombien coûte la création d'un SaaS avec Claude Code en 2026Parallel AI AgentsCoding Agent Injection
speedy_devvkoen_salo
Blog/Handbook/Workflow/Transactional Email

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.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo
speedy_devvWritten by speedy_devvPublished Jul 14, 20269 min readHandbook hubWorkflow index

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-email

resend 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

Continue in Workflow

  • Commerce agentique : comment construire une app que les agents IA peuvent payer
    Un guide en français simple du commerce agentique en 2026 : ce que font x402, ACP et le Machine Payments Protocol, plus un pas-à-pas d'un week-end pour livrer une API payante que les agents IA peuvent acheter.
  • Bonnes pratiques Claude Code
    Cinq habitudes séparent les ingénieurs qui livrent avec Claude Code : les PRDs, les règles CLAUDE.md modulaires, les slash commands personnalisés, les resets /clear, et un état d'esprit d'évolution du système.
  • Le mode auto de Claude Code
    Un second modèle Sonnet examine chaque appel d'outil Claude Code avant qu'il s'exécute. Ce que le mode auto bloque, ce qu'il autorise, et les règles d'autorisation qu'il place dans tes paramètres.
  • Channels, Routines, Teleport, Dispatch
    Les quatre fonctionnalités Claude Code livrées par Anthropic en mars et avril 2026 qui transforment le CLI en une couche de coordination orientée événements, entre téléphone, web et desktop.
  • 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.
  • Changelog Claude Code
    Notes de version par version pour Claude Code, du bêta v0.2 à mars 2026. Bare mode, relais de permissions Channels, corrections OAuth, et chaque breaking change.
  • 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.
  • Templates de prompts qui livrent du code
    Dix recettes de prompts qui livrent du code : scaffolding full-stack, APIs, schémas, tests, refactors, debugging, reviews et CI. Chacun avec les modes d'échec à éviter.

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.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo

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.

On this page

Pourquoi l'email transactionnel est capricieux
Installer Resend et React Email
Obtenir une clé API et vérifier ton domaine d'envoi
Construire un template React Email
Un helper lib/email
Envoyer depuis une Server Action (email de bienvenue après inscription)
Envoyer depuis un Route Handler (réinitialisation de mot de passe)
Construire l'email de reçu
Prévisualiser les templates en local
Gérer les erreurs et l'idempotence
Tester avant d'expédier

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.

Découvrez ce que nous construisons pour les entreprises →