ResendReact EmailNext.jsNestJS

Resend et React Email : envoyer des emails transactionnels élégants depuis Next.js et NestJS

Stack moderne pour vos emails transactionnels : Resend pour l'envoi, React Email pour les templates. Tutoriel complet Next.js 16 et NestJS.

AMAlexis Mouchon5 min de lecture

L'email transactionnel est l'un des points de contact les plus négligés d'un produit web. On bricole un template HTML inline, on bourrine du nodemailer avec un SMTP Gmail, et trois mois plus tard les confirmations de commande finissent en spam. Pour un site vitrine, un SaaS ou un e-commerce, c'est inacceptable : un email qui n'arrive pas, c'est un client perdu.

Depuis dix-huit mois, j'ai adopté la combinaison Resend (envoi) + React Email (templating) sur la quasi-totalité de mes projets Next.js et NestJS. C'est devenu mon défaut, et je vais vous expliquer pourquoi — et comment l'intégrer proprement.

Pourquoi Resend plutôt que SendGrid, Mailgun ou Nodemailer

Avant de plonger dans le code, mettons les choses à plat. Les solutions historiques pour envoyer un email depuis une app Node sont :

Resend, fondé par l'équipe de React Email, est arrivé avec une promesse simple : une API REST claire, un SDK TypeScript first-class, et l'intégration native avec des templates React. Concrètement :

Pour un freelance qui livre des projets clients, c'est un game changer : je peux setup la déliverabilité d'un nouveau domaine en moins de 15 minutes, et le client reçoit un dashboard où il peut suivre ses emails sans dépendre de moi.

Installation et configuration

Côté Resend, on crée un compte, on ajoute son domaine (exemple.com), on copie les enregistrements DNS chez son registrar (OVH, Gandi, Cloudflare...), on attend la vérification, et on génère une API key.

Côté code, on installe les deux paquets :

npm install resend @react-email/components

@react-email/components est la lib qui fournit les composants React optimisés pour les clients mail (Outlook, Gmail, Apple Mail). Elle compile en HTML inline avec les tables et styles inline que ces clients exigent encore en 2026.

Créer un template avec React Email

Le gros intérêt de React Email, c'est de pouvoir écrire ses templates avec la syntaxe JSX qu'on connaît déjà, avec de l'auto-completion TypeScript et de la composition de composants. Voici un template de confirmation de commande :

// emails/order-confirmation.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";

interface OrderConfirmationProps {
  customerName: string;
  orderNumber: string;
  totalAmount: number;
  items: { name: string; quantity: number; price: number }[];
}

export default function OrderConfirmation({
  customerName,
  orderNumber,
  totalAmount,
  items,
}: OrderConfirmationProps) {
  return (
    <Html>
      <Head />
      <Preview>Votre commande {orderNumber} est confirmée</Preview>
      <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
        <Container style={{ padding: "20px", maxWidth: "600px" }}>
          <Heading>Merci {customerName} !</Heading>
          <Text>Votre commande #{orderNumber} a bien été enregistrée.</Text>

          <Section style={{ marginTop: "24px" }}>
            {items.map((item) => (
              <Text key={item.name}>
                {item.quantity} × {item.name} — {item.price.toFixed(2)} €
              </Text>
            ))}
          </Section>

          <Text style={{ fontWeight: "bold", marginTop: "16px" }}>
            Total : {totalAmount.toFixed(2)} €
          </Text>

          <Button
            href="https://exemple.com/commandes"
            style={{
              backgroundColor: "#000",
              color: "#fff",
              padding: "12px 24px",
              borderRadius: "6px",
            }}
          >
            Voir ma commande
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Ce qui est puissant ici, c'est qu'on peut prévisualiser le rendu en dev avec la commande npx react-email dev, ce qui ouvre un serveur local avec un viewer qui simule différents clients. Fini les boucles "envoyer → ouvrir Outlook → constater que c'est cassé".

Intégration côté Next.js : Server Action

Dans une app Next.js 16 avec App Router, le pattern le plus propre est d'envelopper Resend dans une Server Action. On garde l'API key strictement côté serveur, et on évite les API routes inutiles.

// app/actions/send-order-email.ts
"use server";

import { Resend } from "resend";
import OrderConfirmation from "@/emails/order-confirmation";

const resend = new Resend(process.env.RESEND_API_KEY);

interface SendOrderEmailInput {
  to: string;
  customerName: string;
  orderNumber: string;
  totalAmount: number;
  items: { name: string; quantity: number; price: number }[];
}

export async function sendOrderConfirmation(input: SendOrderEmailInput) {
  const { data, error } = await resend.emails.send({
    from: "Boutique <commandes@exemple.com>",
    to: input.to,
    subject: `Confirmation de votre commande #${input.orderNumber}`,
    react: OrderConfirmation({
      customerName: input.customerName,
      orderNumber: input.orderNumber,
      totalAmount: input.totalAmount,
      items: input.items,
    }),
  });

  if (error) {
    console.error("Resend error", error);
    throw new Error("Échec de l'envoi de l'email");
  }

  return data;
}

Le passage du composant React via la clé react est ce qui rend cette stack agréable : Resend gère lui-même le rendu HTML + version texte en fallback. Pas de render() manuel à appeler.

Côté composant client, on appelle ça depuis un useTransition :

"use client";

import { useTransition } from "react";
import { sendOrderConfirmation } from "@/app/actions/send-order-email";

export function ConfirmOrderButton({ order }: { order: Order }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => {
        startTransition(async () => {
          await sendOrderConfirmation({
            to: order.customerEmail,
            customerName: order.customerName,
            orderNumber: order.id,
            totalAmount: order.total,
            items: order.items,
          });
        });
      }}
    >
      {isPending ? "Envoi..." : "Confirmer la commande"}
    </button>
  );
}

Intégration côté NestJS : module dédié

Pour un backend NestJS, je crée systématiquement un MailerModule qui encapsule Resend et expose des méthodes typées par cas d'usage. Ça évite de répandre du resend.emails.send(...) un peu partout.

// src/mailer/mailer.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { Resend } from "resend";
import { MailerService } from "./mailer.service";

@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: "RESEND_CLIENT",
      useFactory: (config: ConfigService) =>
        new Resend(config.getOrThrow<string>("RESEND_API_KEY")),
      inject: [ConfigService],
    },
    MailerService,
  ],
  exports: [MailerService],
})
export class MailerModule {}
// src/mailer/mailer.service.ts
import { Inject, Injectable, Logger } from "@nestjs/common";
import { Resend } from "resend";
import OrderConfirmation from "../emails/order-confirmation";

@Injectable()
export class MailerService {
  private readonly logger = new Logger(MailerService.name);

  constructor(@Inject("RESEND_CLIENT") private readonly resend: Resend) {}

  async sendOrderConfirmation(params: {
    to: string;
    customerName: string;
    orderNumber: string;
    totalAmount: number;
    items: { name: string; quantity: number; price: number }[];
  }) {
    const { data, error } = await this.resend.emails.send({
      from: "Boutique <commandes@exemple.com>",
      to: params.to,
      subject: `Confirmation de votre commande #${params.orderNumber}`,
      react: OrderConfirmation({
        customerName: params.customerName,
        orderNumber: params.orderNumber,
        totalAmount: params.totalAmount,
        items: params.items,
      }),
    });

    if (error) {
      this.logger.error(`Échec d'envoi vers ${params.to}`, error);
      throw error;
    }

    this.logger.log(`Email envoyé : ${data?.id}`);
    return data;
  }
}

L'avantage du pattern, c'est qu'on peut injecter MailerService partout (resolvers GraphQL, controllers REST, listeners d'events) sans dupliquer la logique. Et le jour où on veut changer de provider, on touche un seul fichier.

Asynchrone et résilient : combiner avec BullMQ

Un email transactionnel ne doit jamais bloquer une requête HTTP. Un utilisateur clique sur "Confirmer ma commande", on enregistre la commande, on lui répond 200 OK, et ensuite on envoie l'email. Sinon, si Resend a un hoquet, l'utilisateur attend 30 secondes et finit par rafraîchir, ce qui crée une commande en double.

Dans une app NestJS sérieuse, je passe systématiquement par une queue BullMQ :

// src/mailer/email.processor.ts
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";
import { MailerService } from "./mailer.service";

@Processor("emails")
export class EmailProcessor extends WorkerHost {
  constructor(private readonly mailer: MailerService) {
    super();
  }

  async process(job: Job) {
    switch (job.name) {
      case "order-confirmation":
        return this.mailer.sendOrderConfirmation(job.data);
      default:
        throw new Error(`Unknown job: ${job.name}`);
    }
  }
}
// dans le service métier
await this.emailQueue.add("order-confirmation", {
  to: customer.email,
  customerName: customer.name,
  orderNumber: order.id,
  totalAmount: order.total,
  items: order.items,
}, {
  attempts: 3,
  backoff: { type: "exponential", delay: 5000 },
  removeOnComplete: 100,
});

Avec ce setup, si Resend renvoie un 500 transitoire, BullMQ retente automatiquement 3 fois avec backoff exponentiel. L'API HTTP reste rapide, l'utilisateur n'attend pas, et la commande est confirmée même si l'email part avec 30 secondes de délai.

Bonnes pratiques de déliverabilité

Quelques points qui font la différence entre "mes emails arrivent" et "mes emails finissent dans le dossier promotions" :

  1. Configurer SPF, DKIM et DMARC. Resend fournit les trois enregistrements DNS dès l'ajout du domaine. Ne sautez jamais cette étape — un email envoyé depuis un domaine non vérifié finit en spam dans 60 % des cas.
  2. Utiliser un sous-domaine dédié aux transactionnels (mail.exemple.com ou transactional.exemple.com). Ça isole la réputation : si une campagne marketing pète, vos confirmations de commande continuent de partir.
  3. Toujours inclure un objet précis et un préheader (la prop Preview de React Email). Les boîtes mail affichent ce texte en aperçu, et un préheader bien rédigé augmente sensiblement le taux d'ouverture.
  4. Inclure un lien de désabonnement même pour les transactionnels (RGPD et Yahoo/Gmail l'exigent depuis 2024 pour les expéditeurs en volume).
  5. Surveiller les bounces et complaints via les webhooks Resend. Si un email bounce, on retire l'adresse de la base — sinon, la réputation de l'expéditeur s'effondre.

Webhooks Resend pour suivre l'état

Resend expose des webhooks pour email.sent, email.delivered, email.bounced, email.complained. Voici un controller NestJS minimaliste pour les recevoir :

// src/mailer/webhook.controller.ts
import { Body, Controller, Post } from "@nestjs/common";

@Controller("webhooks/resend")
export class ResendWebhookController {
  @Post()
  async handle(@Body() event: { type: string; data: { email_id: string; to: string[] } }) {
    switch (event.type) {
      case "email.bounced":
      case "email.complained":
        // Marquer l'adresse comme invalide en base
        break;
      case "email.delivered":
        // Log ou métrique
        break;
    }
    return { received: true };
  }
}

En production, n'oubliez pas de vérifier la signature svix-signature que Resend ajoute aux requêtes — sans ça, n'importe qui peut spammer votre endpoint et flagger vos contacts comme bounced.

Synthèse

La stack Resend + React Email résout des problèmes que la combinaison Nodemailer + Handlebars laissait pourrir depuis des années : templating type-safe avec composition React, déliverabilité au niveau enterprise, DX réellement agréable, et tarification raisonnable pour les petits volumes.

Pour aller plus loin, je vous recommande de lire mon article sur BullMQ et NestJS pour le pattern de queue asynchrone, et celui sur Zod dans Next.js et NestJS pour valider les payloads d'emails avant envoi (un email avec un nom client undefined dans l'objet, ça arrive plus souvent qu'on le pense).

Vous avez un projet web — site vitrine, e-commerce, SaaS — qui nécessite des emails transactionnels fiables ? N'hésitez pas à me contacter pour en discuter, je serai ravi de vous accompagner sur l'architecture et l'intégration.