Créer des emails transactionnels avec Resend et React Email dans Next.js

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Toute application web sérieuse doit envoyer des emails — messages de bienvenue, réinitialisations de mot de passe, confirmations de commande, reçus de facture et invitations. Traditionnellement, créer des templates email signifiait se battre avec du CSS inline, des tableaux imbriqués et des tests sur des dizaines de clients email. Une expérience développeur cauchemardesque.

React Email change la donne en vous permettant de construire des templates email avec des composants React — le même modèle mental que celui utilisé pour votre interface utilisateur. Resend fournit une infrastructure de livraison avec une API moderne, une excellente délivrabilité et un niveau gratuit généreux. Combinés avec Next.js, vous obtenez un système email full-stack où les templates vivent aux côtés de votre code applicatif, sont typés et peuvent être prévisualisés dans le navigateur avant envoi.

Dans ce tutoriel, vous allez construire un système complet d'emails transactionnels pour une application SaaS. Vous créerez des templates pour les emails de bienvenue, les réinitialisations de mot de passe et les reçus de facture, les prévisualiserez dans le navigateur avec le hot-reload, les enverrez via des routes API et déploierez le tout en production.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé
  • Un compte Resend — inscrivez-vous sur le site de Resend (le niveau gratuit inclut 3 000 emails/mois)
  • Des connaissances de base en React et TypeScript
  • Une familiarité avec le Next.js App Router
  • Un domaine vérifié (ou utilisez le domaine de test Resend pour le développement)

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez :

  1. Un projet Next.js avec React Email intégré
  2. Trois templates email prêts pour la production (bienvenue, réinitialisation de mot de passe, facture)
  3. Un serveur de prévisualisation local pour développer et tester visuellement les emails
  4. Des routes API pour envoyer des emails via Resend
  5. Des utilitaires typés pour l'envoi d'emails avec gestion d'erreurs
  6. Un déploiement fonctionnel prêt pour la production

Étape 1 : Créer le projet Next.js

Commencez par créer une nouvelle application Next.js :

npx create-next-app@latest saas-emails --typescript --tailwind --eslint --app --src-dir
cd saas-emails

Choisissez les options par défaut lorsque vous y êtes invité. Cela vous donne un projet Next.js 15 avec TypeScript et App Router.

Étape 2 : Installer les dépendances

Installez React Email pour le développement de templates et Resend pour la livraison :

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

Le package @react-email/components fournit tous les blocs de construction — Html, Head, Body, Container, Text, Button, Img, Link, Section, Row, Column, Hr, Preview, et plus encore.

Ajoutez un script à votre package.json pour le serveur de prévisualisation :

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "email": "email dev --dir src/emails --port 3001"
  }
}

Étape 3 : Configurer les variables d'environnement

Créez un fichier .env.local à la racine de votre projet :

RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Obtenez votre clé API depuis le tableau de bord Resend sous API Keys. Pour le développement, vous pouvez utiliser la clé API de test qui envoie les emails uniquement à votre adresse email vérifiée.

Étape 4 : Créer la structure des répertoires email

Organisez vos templates email dans un répertoire dédié :

mkdir -p src/emails/components

La structure de votre projet ressemblera à ceci :

src/
  emails/
    components/
      email-header.tsx
      email-footer.tsx
      email-button.tsx
    welcome.tsx
    password-reset.tsx
    invoice.tsx
  app/
    api/
      email/
        send/
          route.ts
  lib/
    resend.ts

Étape 5 : Créer les composants email partagés

Avant de créer les templates complets, construisez des composants réutilisables qui maintiennent une image de marque cohérente dans tous les emails.

Composant en-tête

Créez src/emails/components/email-header.tsx :

import { Img, Section, Text } from "@react-email/components";
 
interface EmailHeaderProps {
  title?: string;
}
 
export function EmailHeader({ title }: EmailHeaderProps) {
  return (
    <Section style={headerStyle}>
      <Img
        src="https://yourdomain.com/logo.png"
        width="140"
        height="40"
        alt="YourApp"
        style={logoStyle}
      />
      {title && <Text style={titleStyle}>{title}</Text>}
    </Section>
  );
}
 
const headerStyle = {
  textAlign: "center" as const,
  padding: "32px 0 24px",
};
 
const logoStyle = {
  margin: "0 auto",
};
 
const titleStyle = {
  fontSize: "24px",
  fontWeight: "bold" as const,
  color: "#111827",
  margin: "16px 0 0",
};

Composant pied de page

Créez src/emails/components/email-footer.tsx :

import { Hr, Link, Section, Text } from "@react-email/components";
 
export function EmailFooter() {
  return (
    <Section style={footerStyle}>
      <Hr style={dividerStyle} />
      <Text style={footerTextStyle}>
        &copy; 2026 YourApp. Tous droits réservés.
      </Text>
      <Text style={footerLinksStyle}>
        <Link href="https://yourdomain.com/privacy" style={linkStyle}>
          Politique de confidentialité
        </Link>
        {" • "}
        <Link href="https://yourdomain.com/terms" style={linkStyle}>
          Conditions d'utilisation
        </Link>
        {" • "}
        <Link href="https://yourdomain.com/unsubscribe" style={linkStyle}>
          Se désabonner
        </Link>
      </Text>
    </Section>
  );
}
 
const footerStyle = {
  padding: "0 0 32px",
};
 
const dividerStyle = {
  borderColor: "#e5e7eb",
  margin: "32px 0 24px",
};
 
const footerTextStyle = {
  fontSize: "12px",
  color: "#9ca3af",
  textAlign: "center" as const,
};
 
const footerLinksStyle = {
  fontSize: "12px",
  color: "#9ca3af",
  textAlign: "center" as const,
};
 
const linkStyle = {
  color: "#6b7280",
  textDecoration: "underline",
};

Composant bouton réutilisable

Créez src/emails/components/email-button.tsx :

import { Button } from "@react-email/components";
 
interface EmailButtonProps {
  href: string;
  children: React.ReactNode;
  variant?: "primary" | "secondary";
}
 
export function EmailButton({
  href,
  children,
  variant = "primary",
}: EmailButtonProps) {
  const style =
    variant === "primary" ? primaryButtonStyle : secondaryButtonStyle;
 
  return (
    <Button href={href} style={style}>
      {children}
    </Button>
  );
}
 
const primaryButtonStyle = {
  backgroundColor: "#2563eb",
  borderRadius: "8px",
  color: "#ffffff",
  fontSize: "14px",
  fontWeight: "600" as const,
  textDecoration: "none",
  textAlign: "center" as const,
  display: "inline-block",
  padding: "12px 24px",
};
 
const secondaryButtonStyle = {
  backgroundColor: "#ffffff",
  borderRadius: "8px",
  border: "1px solid #d1d5db",
  color: "#374151",
  fontSize: "14px",
  fontWeight: "600" as const,
  textDecoration: "none",
  textAlign: "center" as const,
  display: "inline-block",
  padding: "12px 24px",
};

Étape 6 : Créer le template email de bienvenue

Créez src/emails/welcome.tsx :

import {
  Body,
  Container,
  Head,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";
import { EmailHeader } from "./components/email-header";
import { EmailFooter } from "./components/email-footer";
import { EmailButton } from "./components/email-button";
 
interface WelcomeEmailProps {
  username: string;
  loginUrl?: string;
}
 
export default function WelcomeEmail({
  username = "there",
  loginUrl = "https://yourdomain.com/login",
}: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Bienvenue sur YourApp — commençons !</Preview>
      <Body style={bodyStyle}>
        <Container style={containerStyle}>
          <EmailHeader title="Bienvenue à bord !" />
 
          <Section style={contentStyle}>
            <Text style={greetingStyle}>Bonjour {username},</Text>
 
            <Text style={paragraphStyle}>
              Merci de vous être inscrit sur YourApp ! Nous sommes ravis de
              vous accueillir. Votre compte est prêt et vous pouvez commencer
              à explorer dès maintenant.
            </Text>
 
            <Text style={paragraphStyle}>
              Voici ce que vous pouvez faire ensuite :
            </Text>
 
            <Section style={listStyle}>
              <Text style={listItemStyle}>
                ✅ Compléter vos paramètres de profil
              </Text>
              <Text style={listItemStyle}>
                ✅ Créer votre premier projet
              </Text>
              <Text style={listItemStyle}>
                ✅ Inviter les membres de votre équipe
              </Text>
              <Text style={listItemStyle}>
                ✅ Explorer la documentation
              </Text>
            </Section>
 
            <Section style={buttonContainerStyle}>
              <EmailButton href={loginUrl}>
                Commencer
              </EmailButton>
            </Section>
 
            <Text style={paragraphStyle}>
              Si vous avez des questions, répondez à cet email — nous lisons
              et répondons à chaque message.
            </Text>
 
            <Text style={signoffStyle}>
              Bon développement,
              <br />
              L'équipe YourApp
            </Text>
          </Section>
 
          <EmailFooter />
        </Container>
      </Body>
    </Html>
  );
}
 
const bodyStyle = {
  backgroundColor: "#f9fafb",
  fontFamily:
    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const containerStyle = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  maxWidth: "600px",
  borderRadius: "8px",
  overflow: "hidden" as const,
};
 
const contentStyle = { padding: "0 32px" };
 
const greetingStyle = {
  fontSize: "18px",
  fontWeight: "600" as const,
  color: "#111827",
  margin: "0 0 16px",
};
 
const paragraphStyle = {
  fontSize: "15px",
  lineHeight: "24px",
  color: "#374151",
  margin: "0 0 16px",
};
 
const listStyle = {
  margin: "0 0 24px",
  padding: "16px 24px",
  backgroundColor: "#f9fafb",
  borderRadius: "8px",
};
 
const listItemStyle = {
  fontSize: "14px",
  lineHeight: "28px",
  color: "#374151",
  margin: "0",
};
 
const buttonContainerStyle = {
  textAlign: "center" as const,
  margin: "24px 0",
};
 
const signoffStyle = {
  fontSize: "15px",
  lineHeight: "24px",
  color: "#374151",
  margin: "24px 0 0",
};

Étape 7 : Créer le template de réinitialisation de mot de passe

Créez src/emails/password-reset.tsx :

import {
  Body,
  Container,
  Head,
  Html,
  Preview,
  Section,
  Text,
  Code,
} from "@react-email/components";
import { EmailHeader } from "./components/email-header";
import { EmailFooter } from "./components/email-footer";
import { EmailButton } from "./components/email-button";
 
interface PasswordResetEmailProps {
  username: string;
  resetUrl?: string;
  expiresInMinutes?: number;
}
 
export default function PasswordResetEmail({
  username = "there",
  resetUrl = "https://yourdomain.com/reset?token=abc123",
  expiresInMinutes = 60,
}: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Réinitialisez votre mot de passe YourApp</Preview>
      <Body style={bodyStyle}>
        <Container style={containerStyle}>
          <EmailHeader title="Réinitialisation du mot de passe" />
 
          <Section style={contentStyle}>
            <Text style={paragraphStyle}>Bonjour {username},</Text>
 
            <Text style={paragraphStyle}>
              Nous avons reçu une demande de réinitialisation de votre mot de
              passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau
              mot de passe :
            </Text>
 
            <Section style={buttonContainerStyle}>
              <EmailButton href={resetUrl}>
                Réinitialiser le mot de passe
              </EmailButton>
            </Section>
 
            <Section style={warningBoxStyle}>
              <Text style={warningTextStyle}>
                ⏰ Ce lien expire dans {expiresInMinutes} minutes. Si vous
                n'avez pas demandé de réinitialisation de mot de passe, vous
                pouvez ignorer cet email en toute sécurité.
              </Text>
            </Section>
 
            <Text style={paragraphStyle}>
              Si le bouton ne fonctionne pas, copiez et collez cette URL dans
              votre navigateur :
            </Text>
 
            <Section style={urlBoxStyle}>
              <Code style={urlTextStyle}>{resetUrl}</Code>
            </Section>
 
            <Text style={securityNoteStyle}>
              Pour des raisons de sécurité, cette demande a été reçue depuis
              un navigateur web. Si vous n'avez pas fait cette demande,
              veuillez changer votre mot de passe immédiatement.
            </Text>
          </Section>
 
          <EmailFooter />
        </Container>
      </Body>
    </Html>
  );
}
 
const bodyStyle = {
  backgroundColor: "#f9fafb",
  fontFamily:
    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const containerStyle = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  maxWidth: "600px",
  borderRadius: "8px",
  overflow: "hidden" as const,
};
 
const contentStyle = { padding: "0 32px" };
 
const paragraphStyle = {
  fontSize: "15px",
  lineHeight: "24px",
  color: "#374151",
  margin: "0 0 16px",
};
 
const buttonContainerStyle = {
  textAlign: "center" as const,
  margin: "24px 0",
};
 
const warningBoxStyle = {
  backgroundColor: "#fef3c7",
  borderRadius: "8px",
  padding: "16px",
  margin: "0 0 24px",
};
 
const warningTextStyle = {
  fontSize: "13px",
  lineHeight: "20px",
  color: "#92400e",
  margin: "0",
};
 
const urlBoxStyle = {
  backgroundColor: "#f3f4f6",
  borderRadius: "8px",
  padding: "12px 16px",
  margin: "0 0 24px",
};
 
const urlTextStyle = {
  fontSize: "12px",
  color: "#6b7280",
  wordBreak: "break-all" as const,
};
 
const securityNoteStyle = {
  fontSize: "13px",
  lineHeight: "20px",
  color: "#9ca3af",
  margin: "0 0 16px",
  fontStyle: "italic",
};

Étape 8 : Créer le template email de facture

Créez src/emails/invoice.tsx :

import {
  Body,
  Column,
  Container,
  Head,
  Hr,
  Html,
  Preview,
  Row,
  Section,
  Text,
} from "@react-email/components";
import { EmailHeader } from "./components/email-header";
import { EmailFooter } from "./components/email-footer";
import { EmailButton } from "./components/email-button";
 
interface InvoiceItem {
  description: string;
  quantity: number;
  unitPrice: number;
}
 
interface InvoiceEmailProps {
  customerName: string;
  invoiceNumber: string;
  invoiceDate: string;
  dueDate: string;
  items: InvoiceItem[];
  currency?: string;
  invoiceUrl?: string;
}
 
function formatCurrency(amount: number, currency: string) {
  return new Intl.NumberFormat("fr-FR", {
    style: "currency",
    currency,
  }).format(amount);
}
 
export default function InvoiceEmail({
  customerName = "Jean Dupont",
  invoiceNumber = "INV-2026-001",
  invoiceDate = "15 mars 2026",
  dueDate = "15 avril 2026",
  items = [
    { description: "Plan Pro - Mensuel", quantity: 1, unitPrice: 29 },
    { description: "Places supplémentaires (3)", quantity: 3, unitPrice: 10 },
  ],
  currency = "USD",
  invoiceUrl = "https://yourdomain.com/invoices/INV-2026-001",
}: InvoiceEmailProps) {
  const subtotal = items.reduce(
    (sum, item) => sum + item.quantity * item.unitPrice,
    0
  );
  const tax = subtotal * 0.1;
  const total = subtotal + tax;
 
  return (
    <Html>
      <Head />
      <Preview>
        Facture {invoiceNumber}{formatCurrency(total, currency)} due le{" "}
        {dueDate}
      </Preview>
      <Body style={bodyStyle}>
        <Container style={containerStyle}>
          <EmailHeader title="Facture" />
 
          <Section style={contentStyle}>
            <Text style={paragraphStyle}>Bonjour {customerName},</Text>
            <Text style={paragraphStyle}>
              Voici votre facture. Veuillez trouver les détails ci-dessous :
            </Text>
 
            {/* Métadonnées de la facture */}
            <Section style={metaBoxStyle}>
              <Row>
                <Column>
                  <Text style={metaLabelStyle}>Numéro de facture</Text>
                  <Text style={metaValueStyle}>{invoiceNumber}</Text>
                </Column>
                <Column>
                  <Text style={metaLabelStyle}>Date</Text>
                  <Text style={metaValueStyle}>{invoiceDate}</Text>
                </Column>
                <Column>
                  <Text style={metaLabelStyle}>Échéance</Text>
                  <Text style={metaValueStyle}>{dueDate}</Text>
                </Column>
              </Row>
            </Section>
 
            {/* En-tête du tableau */}
            <Section style={tableHeaderStyle}>
              <Row>
                <Column style={descColStyle}>
                  <Text style={headerCellStyle}>Description</Text>
                </Column>
                <Column style={qtyColStyle}>
                  <Text style={headerCellStyle}>Qté</Text>
                </Column>
                <Column style={priceColStyle}>
                  <Text style={headerCellStyle}>Prix</Text>
                </Column>
                <Column style={totalColStyle}>
                  <Text style={headerCellStyle}>Total</Text>
                </Column>
              </Row>
            </Section>
 
            {/* Lignes */}
            {items.map((item, i) => (
              <Section key={i} style={tableRowStyle}>
                <Row>
                  <Column style={descColStyle}>
                    <Text style={cellStyle}>{item.description}</Text>
                  </Column>
                  <Column style={qtyColStyle}>
                    <Text style={cellStyle}>{item.quantity}</Text>
                  </Column>
                  <Column style={priceColStyle}>
                    <Text style={cellStyle}>
                      {formatCurrency(item.unitPrice, currency)}
                    </Text>
                  </Column>
                  <Column style={totalColStyle}>
                    <Text style={cellStyle}>
                      {formatCurrency(
                        item.quantity * item.unitPrice,
                        currency
                      )}
                    </Text>
                  </Column>
                </Row>
              </Section>
            ))}
 
            {/* Totaux */}
            <Hr style={dividerStyle} />
            <Section style={totalsStyle}>
              <Row>
                <Column style={totalsLabelColStyle}>
                  <Text style={totalsLabelStyle}>Sous-total</Text>
                </Column>
                <Column style={totalsValueColStyle}>
                  <Text style={totalsValueStyle}>
                    {formatCurrency(subtotal, currency)}
                  </Text>
                </Column>
              </Row>
              <Row>
                <Column style={totalsLabelColStyle}>
                  <Text style={totalsLabelStyle}>TVA (10%)</Text>
                </Column>
                <Column style={totalsValueColStyle}>
                  <Text style={totalsValueStyle}>
                    {formatCurrency(tax, currency)}
                  </Text>
                </Column>
              </Row>
              <Hr style={dividerStyle} />
              <Row>
                <Column style={totalsLabelColStyle}>
                  <Text style={grandTotalLabelStyle}>Total dû</Text>
                </Column>
                <Column style={totalsValueColStyle}>
                  <Text style={grandTotalValueStyle}>
                    {formatCurrency(total, currency)}
                  </Text>
                </Column>
              </Row>
            </Section>
 
            <Section style={buttonContainerStyle}>
              <EmailButton href={invoiceUrl}>
                Voir la facture en ligne
              </EmailButton>
            </Section>
 
            <Text style={noteStyle}>
              Le paiement est dû avant le {dueDate}. Si vous avez déjà
              effectué le paiement, veuillez ignorer cet email.
            </Text>
          </Section>
 
          <EmailFooter />
        </Container>
      </Body>
    </Html>
  );
}
 
const bodyStyle = {
  backgroundColor: "#f9fafb",
  fontFamily:
    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const containerStyle = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  maxWidth: "600px",
  borderRadius: "8px",
  overflow: "hidden" as const,
};
 
const contentStyle = { padding: "0 32px" };
 
const paragraphStyle = {
  fontSize: "15px",
  lineHeight: "24px",
  color: "#374151",
  margin: "0 0 16px",
};
 
const metaBoxStyle = {
  backgroundColor: "#f9fafb",
  borderRadius: "8px",
  padding: "16px",
  margin: "0 0 24px",
};
 
const metaLabelStyle = {
  fontSize: "11px",
  fontWeight: "600" as const,
  color: "#9ca3af",
  textTransform: "uppercase" as const,
  margin: "0 0 4px",
};
 
const metaValueStyle = {
  fontSize: "14px",
  fontWeight: "600" as const,
  color: "#111827",
  margin: "0",
};
 
const tableHeaderStyle = {
  backgroundColor: "#f3f4f6",
  borderRadius: "4px",
  padding: "8px 12px",
};
 
const tableRowStyle = { padding: "8px 12px" };
 
const descColStyle = { width: "45%" };
const qtyColStyle = { width: "15%", textAlign: "center" as const };
const priceColStyle = { width: "20%", textAlign: "right" as const };
const totalColStyle = { width: "20%", textAlign: "right" as const };
 
const headerCellStyle = {
  fontSize: "11px",
  fontWeight: "600" as const,
  color: "#6b7280",
  textTransform: "uppercase" as const,
  margin: "0",
};
 
const cellStyle = {
  fontSize: "14px",
  color: "#374151",
  margin: "0",
};
 
const dividerStyle = { borderColor: "#e5e7eb", margin: "8px 0" };
 
const totalsStyle = { padding: "0 12px" };
const totalsLabelColStyle = { width: "70%" };
const totalsValueColStyle = { width: "30%", textAlign: "right" as const };
 
const totalsLabelStyle = {
  fontSize: "14px",
  color: "#6b7280",
  margin: "4px 0",
};
 
const totalsValueStyle = {
  fontSize: "14px",
  color: "#374151",
  margin: "4px 0",
};
 
const grandTotalLabelStyle = {
  fontSize: "16px",
  fontWeight: "bold" as const,
  color: "#111827",
  margin: "4px 0",
};
 
const grandTotalValueStyle = {
  fontSize: "16px",
  fontWeight: "bold" as const,
  color: "#2563eb",
  margin: "4px 0",
};
 
const buttonContainerStyle = {
  textAlign: "center" as const,
  margin: "24px 0",
};
 
const noteStyle = {
  fontSize: "13px",
  color: "#9ca3af",
  textAlign: "center" as const,
  margin: "0 0 16px",
};

Étape 9 : Prévisualiser vos emails localement

Lancez le serveur de prévisualisation React Email :

npm run email

Ouvrez http://localhost:3001 dans votre navigateur. Vous verrez les trois templates email listés. Cliquez sur n'importe quel template pour voir un aperçu en direct. Les modifications de vos fichiers templates se rechargeront instantanément.

Le serveur de prévisualisation vous permet de :

  • Visualiser le rendu de l'email exactement comme il apparaîtra dans les clients email
  • Basculer entre les vues bureau et mobile
  • Copier le code source HTML pour tester dans d'autres outils
  • Envoyer un email de test directement depuis l'interface de prévisualisation
  • Changer les props pour tester différents états

C'est l'un des plus grands avantages de React Email — vous développez des emails avec le même workflow que vos composants UI.

Étape 10 : Configurer le client Resend

Créez src/lib/resend.ts :

import { Resend } from "resend";
 
if (!process.env.RESEND_API_KEY) {
  throw new Error("La variable d'environnement RESEND_API_KEY n'est pas définie");
}
 
export const resend = new Resend(process.env.RESEND_API_KEY);
 
// Expéditeur par défaut — utilisez votre domaine vérifié en production
export const FROM_EMAIL = "YourApp <noreply@yourdomain.com>";

Étape 11 : Créer la route API d'envoi d'emails

Créez src/app/api/email/send/route.ts :

import { NextRequest, NextResponse } from "next/server";
import { resend, FROM_EMAIL } from "@/lib/resend";
import WelcomeEmail from "@/emails/welcome";
import PasswordResetEmail from "@/emails/password-reset";
import InvoiceEmail from "@/emails/invoice";
 
type EmailTemplate = "welcome" | "password-reset" | "invoice";
 
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { template, to, data } = body as {
      template: EmailTemplate;
      to: string;
      data: Record<string, unknown>;
    };
 
    if (!template || !to) {
      return NextResponse.json(
        { error: "Champs requis manquants : template, to" },
        { status: 400 }
      );
    }
 
    const emailConfig = getEmailConfig(template, data);
 
    if (!emailConfig) {
      return NextResponse.json(
        { error: `Template inconnu : ${template}` },
        { status: 400 }
      );
    }
 
    const { data: result, error } = await resend.emails.send({
      from: FROM_EMAIL,
      to: [to],
      subject: emailConfig.subject,
      react: emailConfig.component,
    });
 
    if (error) {
      console.error("Erreur Resend :", error);
      return NextResponse.json(
        { error: "Échec de l'envoi de l'email" },
        { status: 500 }
      );
    }
 
    return NextResponse.json({ success: true, id: result?.id });
  } catch (err) {
    console.error("Erreur d'envoi d'email :", err);
    return NextResponse.json(
      { error: "Erreur interne du serveur" },
      { status: 500 }
    );
  }
}
 
function getEmailConfig(
  template: EmailTemplate,
  data: Record<string, unknown>
) {
  switch (template) {
    case "welcome":
      return {
        subject: "Bienvenue sur YourApp !",
        component: WelcomeEmail({
          username: (data.username as string) || "there",
          loginUrl: data.loginUrl as string,
        }),
      };
 
    case "password-reset":
      return {
        subject: "Réinitialisez votre mot de passe",
        component: PasswordResetEmail({
          username: (data.username as string) || "there",
          resetUrl: data.resetUrl as string,
          expiresInMinutes: (data.expiresInMinutes as number) || 60,
        }),
      };
 
    case "invoice":
      return {
        subject: `Facture ${data.invoiceNumber || ""}`,
        component: InvoiceEmail({
          customerName: (data.customerName as string) || "Client",
          invoiceNumber: (data.invoiceNumber as string) || "INV-000",
          invoiceDate: (data.invoiceDate as string) || new Date().toLocaleDateString(),
          dueDate: (data.dueDate as string) || "",
          items: (data.items as Array<{
            description: string;
            quantity: number;
            unitPrice: number;
          }>) || [],
          currency: (data.currency as string) || "USD",
          invoiceUrl: data.invoiceUrl as string,
        }),
      };
 
    default:
      return null;
  }
}

Étape 12 : Créer un utilitaire d'envoi typé

Pour une meilleure expérience développeur, créez un utilitaire qui offre la sécurité des types lors de l'envoi d'emails. Créez src/lib/send-email.ts :

import { resend, FROM_EMAIL } from "./resend";
import WelcomeEmail from "@/emails/welcome";
import PasswordResetEmail from "@/emails/password-reset";
import InvoiceEmail from "@/emails/invoice";
 
interface WelcomeEmailData {
  username: string;
  loginUrl?: string;
}
 
interface PasswordResetData {
  username: string;
  resetUrl: string;
  expiresInMinutes?: number;
}
 
interface InvoiceData {
  customerName: string;
  invoiceNumber: string;
  invoiceDate: string;
  dueDate: string;
  items: Array<{
    description: string;
    quantity: number;
    unitPrice: number;
  }>;
  currency?: string;
  invoiceUrl?: string;
}
 
type EmailMap = {
  welcome: WelcomeEmailData;
  "password-reset": PasswordResetData;
  invoice: InvoiceData;
};
 
const templateMap = {
  welcome: {
    subject: "Bienvenue sur YourApp !",
    render: (data: WelcomeEmailData) => WelcomeEmail(data),
  },
  "password-reset": {
    subject: "Réinitialisez votre mot de passe",
    render: (data: PasswordResetData) => PasswordResetEmail(data),
  },
  invoice: {
    subject: (data: InvoiceData) => `Facture ${data.invoiceNumber}`,
    render: (data: InvoiceData) => InvoiceEmail(data),
  },
};
 
export async function sendEmail<T extends keyof EmailMap>(
  template: T,
  to: string,
  data: EmailMap[T]
) {
  const config = templateMap[template];
  const subject =
    typeof config.subject === "function"
      ? config.subject(data as InvoiceData)
      : config.subject;
 
  const { data: result, error } = await resend.emails.send({
    from: FROM_EMAIL,
    to: [to],
    subject,
    react: config.render(data as never),
  });
 
  if (error) {
    throw new Error(`Échec de l'envoi de l'email ${template} : ${error.message}`);
  }
 
  return result;
}

Maintenant vous pouvez l'utiliser partout dans votre code serveur avec une sécurité de types complète :

// Dans un Server Action ou une route API
import { sendEmail } from "@/lib/send-email";
 
// TypeScript sait exactement quelles données chaque template nécessite
await sendEmail("welcome", "user@example.com", {
  username: "Alice",
  loginUrl: "https://yourdomain.com/login",
});
 
await sendEmail("password-reset", "user@example.com", {
  username: "Alice",
  resetUrl: "https://yourdomain.com/reset?token=xyz",
  expiresInMinutes: 30,
});
 
await sendEmail("invoice", "facturation@entreprise.com", {
  customerName: "Acme Corp",
  invoiceNumber: "INV-2026-042",
  invoiceDate: "15 mars 2026",
  dueDate: "15 avril 2026",
  items: [
    { description: "Plan Pro", quantity: 1, unitPrice: 29 },
  ],
});

Étape 13 : Utiliser les emails dans les Server Actions

Créez un Server Action qui envoie un email de bienvenue quand un utilisateur s'inscrit. Créez src/app/actions/auth.ts :

"use server";
 
import { sendEmail } from "@/lib/send-email";
 
export async function signUpAction(formData: FormData) {
  const email = formData.get("email") as string;
  const name = formData.get("name") as string;
 
  // ... votre logique de création d'utilisateur ici ...
 
  // Envoyer l'email de bienvenue
  try {
    await sendEmail("welcome", email, {
      username: name,
      loginUrl: `https://yourdomain.com/login`,
    });
  } catch (error) {
    // Logger mais ne pas faire échouer l'inscription si l'email échoue
    console.error("Échec de l'envoi de l'email de bienvenue :", error);
  }
 
  return { success: true };
}

Étape 14 : Tester la livraison des emails

Testez votre route API avec curl :

curl -X POST http://localhost:3000/api/email/send \
  -H "Content-Type: application/json" \
  -d '{
    "template": "welcome",
    "to": "votre-email@example.com",
    "data": {
      "username": "Utilisateur Test"
    }
  }'

Vous devriez recevoir l'email de bienvenue dans votre boîte de réception. Consultez le tableau de bord Resend pour voir le statut de livraison, les taux d'ouverture et les informations de rebond.

Étape 15 : Ajouter des webhooks pour le suivi de livraison

Resend fournit des webhooks pour suivre les événements email. Créez src/app/api/email/webhook/route.ts :

import { NextRequest, NextResponse } from "next/server";
 
interface ResendWebhookEvent {
  type:
    | "email.sent"
    | "email.delivered"
    | "email.bounced"
    | "email.complained"
    | "email.opened"
    | "email.clicked";
  data: {
    email_id: string;
    to: string[];
    created_at: string;
  };
}
 
export async function POST(request: NextRequest) {
  const body = (await request.json()) as ResendWebhookEvent;
 
  switch (body.type) {
    case "email.delivered":
      console.log(`Email ${body.data.email_id} livré à ${body.data.to}`);
      // Mettre à jour votre base de données
      break;
 
    case "email.bounced":
      console.log(`Email ${body.data.email_id} rebondi`);
      // Marquer l'adresse email comme invalide
      break;
 
    case "email.complained":
      console.log(`Plainte spam pour ${body.data.email_id}`);
      // Désinscrire immédiatement l'utilisateur
      break;
 
    case "email.opened":
      console.log(`Email ${body.data.email_id} ouvert`);
      // Suivre l'engagement
      break;
  }
 
  return NextResponse.json({ received: true });
}

Enregistrez cette URL webhook dans votre tableau de bord Resend sous Webhooks.

Dépannage

Problèmes courants et solutions :

Les emails n'arrivent pas dans la boîte de réception :

  • Vérifiez votre tableau de bord Resend pour le statut de livraison
  • Vérifiez les enregistrements DNS de votre domaine (SPF, DKIM, DMARC)
  • En développement, utilisez le domaine de test Resend qui envoie uniquement aux adresses vérifiées

La prévisualisation React Email ne se charge pas :

  • Assurez-vous que le port 3001 n'est pas utilisé
  • Vérifiez que vos fichiers email exportent un composant par défaut
  • Vérifiez que toutes les importations de @react-email/components sont correctes

Erreurs TypeScript dans les templates email :

  • Les templates email utilisent des styles inline (pas Tailwind) — utilisez les types React.CSSProperties
  • La prop style sur les composants React Email attend des noms de propriétés CSS spécifiques

Les emails sont différents selon les clients :

  • Testez toujours avec plusieurs clients (Gmail, Outlook, Apple Mail)
  • Utilisez des tableaux pour les mises en page complexes — certains clients ne supportent pas flexbox ou grid
  • Utilisez des styles inline — le CSS externe est supprimé par la plupart des clients email
  • Gardez les images sous 600px de large

Prochaines étapes

  • Ajoutez l'envoi par lot avec resend.batch.send() pour les newsletters
  • Implémentez la planification d'emails avec le paramètre scheduledAt
  • Configurez des tests A/B pour les lignes d'objet avec le support intégré de Resend
  • Ajoutez la gestion du désabonnement avec les en-têtes de désabonnement en un clic
  • Explorez Resend Audiences pour gérer les listes d'abonnés
  • Construisez une page de préférences de notifications pour permettre aux utilisateurs de contrôler les emails qu'ils reçoivent

Conclusion

Vous avez construit un système complet d'emails transactionnels avec React Email, Resend et Next.js. Vos emails sont créés avec des composants React, typés, prévisualisables dans le navigateur pendant le développement et livrés de manière fiable via l'infrastructure de Resend.

La combinaison du modèle composant de React Email et de l'API de livraison moderne de Resend vous offre la meilleure expérience développeur pour l'email dans l'écosystème JavaScript. Les templates sont simplement des composants React — vous pouvez les composer, partager des props et les tester exactement comme votre code UI. Plus de hacks de tableaux inline ou de copier-coller de HTML entre outils.

Au fur et à mesure que votre application grandit, cette fondation évolue avec vous — ajoutez de nouveaux templates en créant de nouveaux composants React, étendez l'utilitaire sendEmail avec de nouveaux types de templates et surveillez tout via le tableau de bord Resend.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Vitest et React Testing Library avec Next.js 15 : Le Guide Complet des Tests Unitaires en 2026.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Créer des APIs Type-Safe de bout en bout avec tRPC et Next.js App Router

Apprenez à créer des APIs entièrement type-safe avec tRPC et Next.js 15 App Router. Ce tutoriel pratique couvre la configuration du routeur, les procédures, le middleware, l'intégration de React Query et les appels côté serveur — le tout sans écrire un seul schéma d'API.

28 min read·

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·