·Ingénierie·8 min

Emails transactionnels dans Next.js avec Resend et React Email : guide complet 2026

Envoyez des emails fiables dans Next.js avec Resend et React Email : templates typés, Server Actions, idempotence et deliverability. Guide complet 2026.

L'envoi d'emails transactionnels est souvent l'une des dernières choses implémentées dans une app Next.js — et pourtant c'est l'une des premières qui plante en production. SMTP mal configuré, HTML qui casse dans Outlook, emails qui finissent en spam. Resend change la donne : une API épurée, des logs en temps réel, et un SDK TypeScript first-class.

Couplé à React Email, vous écrivez vos templates comme des composants React — props typées, prévisualisation live, et hot-reload en développement. Ce guide couvre l'intégration complète dans un projet Next.js App Router : setup, templates, envoi depuis Server Actions, gestion des erreurs, webhooks et deliverability.

Aperçu d'un email transactionnel bien structuré dans un client mail

Pourquoi Resend plutôt que Nodemailer ou SendGrid

Nodemailer reste populaire, mais il impose de gérer un serveur SMTP, la configuration TLS, et les queues manuellement. SendGrid et Mailgun sont plus robustes, mais leurs APIs vieillissent — webhooks complexes, SDKs lourds, dashboards confus.

Resend a été conçu pour les développeurs modernes. Le SDK TypeScript est first-class, l'API REST est claire, et l'intégration avec React Email rend la création de templates agréable. En production, les logs Resend incluent les événements de livraison, d'ouverture et de clic — directement dans le dashboard, sans configuration supplémentaire.

Côté coûts : le plan gratuit couvre 3 000 emails/mois et 100 par jour, suffisant pour le développement et les projets early-stage. La réputation d'envoi est gérée par Resend — pas besoin de monitorer votre propre IP ni de traiter les bounces manuellement.

Installer Resend et React Email dans Next.js

bun add resend @react-email/components react-email

Ajoutez votre clé API dans .env.local :

RESEND_API_KEY=re_xxxxxxxxxxxx

Pour prévisualiser les templates en local, ajoutez le script dans package.json :

{
  "scripts": {
    "email": "email dev --dir src/emails --port 3001"
  }
}

Lancez bun email et ouvrez http://localhost:3001 — React Email Studio démarre avec hot-reload. Chaque modification de template est visible instantanément, avec inspection du HTML généré et des styles inline appliqués.

Créer un template avec React Email

React Email fournit des composants cross-client (Html, Body, Section, Text, Button) qui génèrent du HTML compatible Outlook, Gmail, Apple Mail et tous les grands clients email. Plus de hacks CSS ni de tables imbriquées à la main.

// src/emails/WelcomeEmail.tsx
import {
  Body, Button, Container, Head, Html,
  Preview, Section, Text,
} from "@react-email/components";
 
interface WelcomeEmailProps {
  username: string;
  confirmUrl: string;
}
 
export function WelcomeEmail({ username, confirmUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Bienvenue {username} — confirmez votre adresse email</Preview>
      <Body style={{ fontFamily: "sans-serif", background: "#f4f4f4" }}>
        <Container style={{ maxWidth: "600px", margin: "0 auto", padding: "20px" }}>
          <Section>
            <Text style={{ fontSize: "24px", fontWeight: "bold" }}>
              Bienvenue, {username} !
            </Text>
            <Text>
              Pour activer votre compte, confirmez votre adresse email.
            </Text>
            <Button
              href={confirmUrl}
              style={{
                background: "#000",
                color: "#fff",
                padding: "12px 24px",
                borderRadius: "6px",
              }}
            >
              Confirmer mon email
            </Button>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

Les props sont typées — si vous oubliez de passer confirmUrl, TypeScript vous le signale avant même d'exécuter le code. C'est la différence fondamentale avec les templates Handlebars ou Nunjucks : le compilateur devient votre premier testeur.

Envoyer depuis une Server Action

La meilleure intégration dans Next.js App Router est via une Server Action : aucun endpoint API exposé, validation côté serveur, et l'email est envoyé dans le même contexte que la mutation de base de données.

// src/actions/auth.ts
"use server";
 
import { Resend } from "resend";
import { WelcomeEmail } from "@/emails/WelcomeEmail";
 
const resend = new Resend(process.env.RESEND_API_KEY);
 
export async function sendWelcomeEmail(userId: string) {
  const user = await db.user.findUnique({ where: { id: userId } });
  if (!user) throw new Error("User not found");
 
  const confirmUrl = `${process.env.NEXT_PUBLIC_APP_URL}/auth/confirm?token=${user.confirmToken}`;
 
  const { data, error } = await resend.emails.send({
    from: "Kreio <hello@kreio.fr>",
    to: [user.email],
    subject: `Bienvenue, ${user.name} — confirmez votre email`,
    // Resend accepte directement un composant React — pas besoin de render() manuel
    react: WelcomeEmail({ username: user.name, confirmUrl }),
  });
 
  if (error) {
    console.error("[email] send failed", error);
    return { success: false, error: error.message };
  }
 
  return { success: true, emailId: data?.id };
}

Resend se charge du rendu HTML côté serveur. Pas besoin d'appeler render() manuellement sauf pour des usages spécifiques (HTML brut pour logs, génération PDF, etc.).

Idempotence et gestion des erreurs

Un email de bienvenue envoyé deux fois est une mauvaise expérience. Un email de facture envoyé zéro fois est un problème légal. L'idempotence n'est pas optionnelle en production.

La stratégie la plus robuste : stocker l'emailId retourné par Resend en base de données, et vérifier son existence avant tout renvoi.

// Envoyer une facture une seule fois, même si la Server Action est retentée
export async function sendInvoiceEmail(invoiceId: string) {
  const invoice = await db.invoice.findUnique({
    where: { id: invoiceId },
    include: { customer: true },
  });
 
  // Guard : ne pas renvoyer si déjà traité
  if (invoice?.emailSentAt) return { alreadySent: true };
 
  const { data, error } = await resend.emails.send({
    from: "Kreio Facturation <facturation@kreio.fr>",
    to: [invoice!.customer.email],
    subject: `Facture #${invoice!.number}`,
    react: InvoiceEmail({ invoice: invoice! }),
    headers: { "X-Entity-Ref-ID": invoiceId },
  });
 
  if (error) throw new Error(`Email failed: ${error.message}`);
 
  // Marquer comme envoyé de manière atomique
  await db.invoice.update({
    where: { id: invoiceId },
    data: { emailSentAt: new Date(), resendEmailId: data?.id },
  });
 
  return { success: true };
}

Pour les emails critiques, coupler Resend avec une queue Redis/Upstash permet de retry automatiquement en cas d'échec réseau sans risque de doublon.

Webhooks Resend : tracer les événements de livraison

Resend expose des webhooks pour email.sent, email.delivered, email.bounced et email.complained. Configurez-les depuis le dashboard et traitez-les dans un Route Handler :

// src/app/api/webhooks/resend/route.ts
import { Webhook } from "svix";
import { NextRequest, NextResponse } from "next/server";
 
export async function POST(req: NextRequest) {
  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);
 
  const wh = new Webhook(process.env.RESEND_WEBHOOK_SECRET!);
  let event: { type: string; data: { to: string[] } };
 
  try {
    event = wh.verify(payload, headers) as typeof event;
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }
 
  if (event.type === "email.bounced") {
    await db.user.updateMany({
      where: { email: { in: event.data.to } },
      data: { emailBounced: true },
    });
  }
 
  return NextResponse.json({ received: true });
}

La vérification de signature avec svix est indispensable — même logique que pour les webhooks Stripe. Sans elle, n'importe qui peut envoyer de faux événements à votre endpoint.

Deliverability : SPF, DKIM et DMARC

La meilleure API d'envoi ne sert à rien si vos emails finissent dans les spams. Trois enregistrements DNS sont indispensables avant le premier envoi en production.

SPF autorise vos serveurs d'envoi. Resend vous donne l'enregistrement TXT exact à publier sur votre domaine.

DKIM signe cryptographiquement chaque email. Resend génère la paire de clés — vous publiez la clé publique en DNS. Les serveurs destinataires vérifient la signature à la réception.

DMARC indique aux serveurs de réception quoi faire si SPF ou DKIM échoue. Commencez en mode none (monitoring seul) pendant 2 à 3 semaines, puis passez en quarantine ou reject.

Vérifiez votre configuration avec mail-tester.com avant le premier envoi. Un score de 10/10 n'est pas un luxe — c'est la condition d'une deliverability correcte sur Gmail et Outlook.

En pratique

Sur les projets Kreio en production, le client Resend est initialisé en singleton dans src/lib/resend.ts — une seule instance partagée, pas de réinstanciation à chaque requête. Tous les templates vivent dans src/emails/, typés et prévisualisables avec bun email.

Pour les notifications non-critiques (résumés hebdomadaires, newsletters), l'envoi est délégué à une queue asynchrone plutôt qu'exécuté de façon synchrone dans la Server Action. L'utilisateur reçoit la réponse immédiatement, l'email part dans les secondes qui suivent. Ce pattern rejoint celui des nouveaux hooks React 19 avec useOptimistic : l'UI s'actualise avant même la confirmation serveur.

Pour les emails avec pièce jointe (factures PDF), on combine Resend avec @react-pdf/renderer : le PDF est généré côté serveur, converti en Buffer, et passé dans le tableau attachments de resend.emails.send(). Aucun stockage temporaire, aucun service externe supplémentaire.

Sources

Cet article a été rédigé avec l'aide de l'IA et relu par un humain avant publication.