Blog/Tutorials & Step-by-Step/Build a Transactional Email System with React Email and Resend
POST
December 22, 2025
LAST UPDATEDDecember 22, 2025

Build a Transactional Email System with React Email and Resend

Build a production-ready transactional email system using React Email for templating and Resend for delivery, with preview workflows, dynamic data, and error handling.

Tags

React EmailResendEmailNode.js
Build a Transactional Email System with React Email and Resend
4 min read

Build a Transactional Email System with React Email and Resend

TL;DR

React Email lets you build email templates as React components with hot reload preview, and Resend handles reliable delivery -- replacing clunky HTML email builders with a modern developer experience.

Prerequisites

  • Node.js 18+ installed
  • A Resend account (free tier available at resend.com)
  • Basic React and TypeScript knowledge
  • A verified domain or Resend's sandbox for testing

Step 1: Project Setup

Initialize a new project and install the required packages.

bash
mkdir email-system && cd email-system
npm init -y
npm install resend @react-email/components react react-dom
npm install -D typescript @types/react @types/react-dom tsx

Create a tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

Set up the project structure:

email-system/
├── src/
│   ├── emails/          # React Email templates
│   ├── lib/             # Resend client and helpers
│   └── send.ts          # Entry point for sending
├── tsconfig.json
└── package.json

Step 2: Configure the Resend Client

Create a Resend client wrapper that handles initialization and error handling.

typescript
// src/lib/resend.ts
import { Resend } from "resend";
 
if (!process.env.RESEND_API_KEY) {
  throw new Error("RESEND_API_KEY environment variable is required");
}
 
export const resend = new Resend(process.env.RESEND_API_KEY);
 
interface SendEmailOptions {
  to: string | string[];
  subject: string;
  react: React.ReactElement;
  from?: string;
}
 
export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
  const { data, error } = await resend.emails.send({
    from: from || "Your App <noreply@yourdomain.com>",
    to: Array.isArray(to) ? to : [to],
    subject,
    react,
  });
 
  if (error) {
    console.error("Failed to send email:", error);
    throw new Error(`Email delivery failed: ${error.message}`);
  }
 
  return data;
}

Step 3: Build the Welcome Email Template

React Email components compile to cross-client compatible HTML. You write JSX, and it outputs HTML that works in Gmail, Outlook, and Apple Mail.

tsx
// src/emails/WelcomeEmail.tsx
import {
  Html,
  Head,
  Body,
  Container,
  Section,
  Text,
  Button,
  Heading,
  Hr,
  Preview,
  Img,
} from "@react-email/components";
 
interface WelcomeEmailProps {
  userName: string;
  loginUrl: string;
}
 
export function WelcomeEmail({ userName, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to our platform, {userName}!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Welcome aboard, {userName}!</Heading>
          <Text style={text}>
            We are excited to have you on board. Your account has been created
            successfully and you are ready to get started.
          </Text>
          <Section style={buttonContainer}>
            <Button style={button} href={loginUrl}>
              Go to Dashboard
            </Button>
          </Section>
          <Hr style={hr} />
          <Text style={footer}>
            If you did not create this account, you can safely ignore this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}
 
// Inline styles are required for email compatibility
const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "40px 20px",
  maxWidth: "560px",
  borderRadius: "8px",
};
 
const h1 = {
  color: "#1a1a1a",
  fontSize: "24px",
  fontWeight: "bold" as const,
  margin: "0 0 16px",
};
 
const text = {
  color: "#4a4a4a",
  fontSize: "16px",
  lineHeight: "26px",
  margin: "0 0 16px",
};
 
const buttonContainer = {
  textAlign: "center" as const,
  margin: "24px 0",
};
 
const button = {
  backgroundColor: "#2563eb",
  borderRadius: "6px",
  color: "#fff",
  fontSize: "16px",
  fontWeight: "bold" as const,
  textDecoration: "none",
  textAlign: "center" as const,
  padding: "12px 24px",
};
 
const hr = {
  borderColor: "#e6e6e6",
  margin: "24px 0",
};
 
const footer = {
  color: "#8c8c8c",
  fontSize: "12px",
};
 
export default WelcomeEmail;

Why Inline Styles?

Email clients strip <style> tags and ignore external stylesheets. React Email handles this by inlining styles into each element during compilation, so your styles actually reach the recipient.

Step 4: Build an Invoice Email Template

Transactional emails often include dynamic data like line items. Here is an invoice template that maps over an array of items.

tsx
// src/emails/InvoiceEmail.tsx
import {
  Html,
  Head,
  Body,
  Container,
  Section,
  Text,
  Heading,
  Hr,
  Preview,
  Row,
  Column,
} from "@react-email/components";
 
interface LineItem {
  description: string;
  quantity: number;
  unitPrice: number;
}
 
interface InvoiceEmailProps {
  customerName: string;
  invoiceNumber: string;
  invoiceDate: string;
  dueDate: string;
  items: LineItem[];
  currency?: string;
}
 
export function InvoiceEmail({
  customerName,
  invoiceNumber,
  invoiceDate,
  dueDate,
  items,
  currency = "USD",
}: InvoiceEmailProps) {
  const total = items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
 
  const formatCurrency = (amount: number) =>
    new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
 
  return (
    <Html>
      <Head />
      <Preview>Invoice #{invoiceNumber} - {formatCurrency(total)}</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Invoice #{invoiceNumber}</Heading>
          <Text style={text}>Hi {customerName},</Text>
          <Text style={text}>
            Here is your invoice for the services rendered. Payment is due by {dueDate}.
          </Text>
 
          <Section style={metaSection}>
            <Row>
              <Column><Text style={metaLabel}>Invoice Date</Text></Column>
              <Column><Text style={metaValue}>{invoiceDate}</Text></Column>
            </Row>
            <Row>
              <Column><Text style={metaLabel}>Due Date</Text></Column>
              <Column><Text style={metaValue}>{dueDate}</Text></Column>
            </Row>
          </Section>
 
          <Hr style={hr} />
 
          {/* Table header */}
          <Section>
            <Row>
              <Column style={tableHeader}>Description</Column>
              <Column style={{ ...tableHeader, textAlign: "center" as const }}>Qty</Column>
              <Column style={{ ...tableHeader, textAlign: "right" as const }}>Amount</Column>
            </Row>
          </Section>
 
          {/* Line items */}
          {items.map((item, index) => (
            <Section key={index}>
              <Row>
                <Column style={tableCell}>{item.description}</Column>
                <Column style={{ ...tableCell, textAlign: "center" as const }}>
                  {item.quantity}
                </Column>
                <Column style={{ ...tableCell, textAlign: "right" as const }}>
                  {formatCurrency(item.quantity * item.unitPrice)}
                </Column>
              </Row>
            </Section>
          ))}
 
          <Hr style={hr} />
 
          <Section>
            <Row>
              <Column />
              <Column style={totalLabel}>Total</Column>
              <Column style={totalValue}>{formatCurrency(total)}</Column>
            </Row>
          </Section>
 
          <Hr style={hr} />
          <Text style={footer}>
            Questions about this invoice? Reply to this email or contact support.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}
 
const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
 
const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "40px 20px",
  maxWidth: "600px",
  borderRadius: "8px",
};
 
const h1 = { color: "#1a1a1a", fontSize: "24px", fontWeight: "bold" as const };
const text = { color: "#4a4a4a", fontSize: "16px", lineHeight: "26px" };
const metaSection = { margin: "16px 0" };
const metaLabel = { color: "#8c8c8c", fontSize: "14px", margin: "4px 0" };
const metaValue = { color: "#1a1a1a", fontSize: "14px", margin: "4px 0", textAlign: "right" as const };
const hr = { borderColor: "#e6e6e6", margin: "24px 0" };
const tableHeader = { color: "#8c8c8c", fontSize: "12px", fontWeight: "bold" as const, padding: "8px 0", textTransform: "uppercase" as const };
const tableCell = { color: "#1a1a1a", fontSize: "14px", padding: "8px 0" };
const totalLabel = { color: "#1a1a1a", fontSize: "16px", fontWeight: "bold" as const, textAlign: "right" as const, padding: "8px 16px 8px 0" };
const totalValue = { color: "#1a1a1a", fontSize: "16px", fontWeight: "bold" as const, textAlign: "right" as const, padding: "8px 0" };
const footer = { color: "#8c8c8c", fontSize: "12px" };
 
export default InvoiceEmail;

Step 5: Set Up the Preview Server

React Email includes a development server that renders your templates in the browser with hot reload.

bash
npm install -D react-email

Add the preview script to package.json:

json
{
  "scripts": {
    "email:dev": "email dev --dir src/emails --port 3001",
    "email:export": "email export --dir src/emails --outDir dist/emails"
  }
}

Run npm run email:dev and open http://localhost:3001 in your browser. You will see a sidebar listing all your email templates. Click any template to preview it, and changes to the source files will hot reload in the browser.

Passing Preview Props

Create a .react-email/ config or export default props directly from your templates:

tsx
// At the bottom of WelcomeEmail.tsx
WelcomeEmail.PreviewProps = {
  userName: "John Doe",
  loginUrl: "https://app.example.com/dashboard",
} satisfies WelcomeEmailProps;

The preview server automatically uses PreviewProps to render the template with sample data.

Step 6: Send Emails from Your Application

Create a service layer that ties templates to delivery.

typescript
// src/lib/emailService.ts
import { sendEmail } from "./resend";
import { WelcomeEmail } from "../emails/WelcomeEmail";
import { InvoiceEmail } from "../emails/InvoiceEmail";
import type { ReactElement } from "react";
 
export const emailService = {
  async sendWelcome(to: string, userName: string) {
    return sendEmail({
      to,
      subject: `Welcome to our platform, ${userName}!`,
      react: WelcomeEmail({ userName, loginUrl: "https://app.example.com/dashboard" }) as ReactElement,
    });
  },
 
  async sendInvoice(
    to: string,
    invoice: {
      customerName: string;
      invoiceNumber: string;
      invoiceDate: string;
      dueDate: string;
      items: { description: string; quantity: number; unitPrice: number }[];
    }
  ) {
    const total = invoice.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0);
    return sendEmail({
      to,
      subject: `Invoice #${invoice.invoiceNumber} - $${total.toFixed(2)}`,
      react: InvoiceEmail(invoice) as ReactElement,
    });
  },
};

Usage in an API Route

If you are using Next.js, call the service from a route handler:

typescript
// app/api/auth/register/route.ts
import { emailService } from "@/lib/emailService";
import { NextResponse } from "next/server";
 
export async function POST(req: Request) {
  const { email, name } = await req.json();
 
  // ... create user in database ...
 
  await emailService.sendWelcome(email, name);
 
  return NextResponse.json({ message: "User created" });
}

Step 7: Handle Errors and Retries

Production email systems need retry logic and error tracking.

typescript
// src/lib/resilientSend.ts
import { sendEmail } from "./resend";
 
interface RetryOptions {
  maxRetries: number;
  baseDelay: number;
}
 
export async function sendWithRetry(
  options: Parameters<typeof sendEmail>[0],
  retryConfig: RetryOptions = { maxRetries: 3, baseDelay: 1000 }
) {
  let lastError: Error | null = null;
 
  for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
    try {
      const result = await sendEmail(options);
      return result;
    } catch (error) {
      lastError = error as Error;
      if (attempt < retryConfig.maxRetries) {
        const delay = retryConfig.baseDelay * Math.pow(2, attempt);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }
 
  throw new Error(
    `Failed to send email after ${retryConfig.maxRetries + 1} attempts: ${lastError?.message}`
  );
}

Putting It All Together

Your email system now has three layers:

  1. Templates -- React Email components in src/emails/ that define layout and accept typed props
  2. Client -- Resend SDK wrapper in src/lib/ that handles delivery, retries, and errors
  3. Service -- Business logic layer that maps application events (user signup, invoice created) to the correct template and recipient

The development workflow is straightforward: edit templates with the preview server running, see changes instantly in the browser, and deploy the same components that get compiled to production HTML.

Next Steps

  • Add a password reset email -- create a template with a time-limited token link
  • Set up webhooks -- use Resend webhooks to track delivery, bounces, and opens
  • Batch sending -- use resend.batch.send() for sending to multiple recipients efficiently
  • Add attachments -- Resend supports file attachments for invoice PDFs
  • Internationalization -- create locale-specific template variants or use i18n interpolation in templates

FAQ

What is React Email?

React Email is an open-source library that lets you build email templates using React components. It compiles JSX into cross-client compatible HTML, so you get component reusability and a local preview server while producing emails that render correctly in Gmail, Outlook, and Apple Mail.

Why use Resend instead of SendGrid or SES?

Resend offers a developer-first API with excellent TypeScript support, built-in React Email integration, and simpler pricing. It handles deliverability, bounce tracking, and analytics with a cleaner DX than legacy providers like SendGrid.

How do you test email templates locally?

React Email includes a local dev server that renders your templates in the browser with hot reload. You can pass sample props to preview different states, and Resend provides a test mode that logs emails without sending them in development.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Add Observability to a Node.js App with OpenTelemetry
Mar 21, 20265 min read
Node.js
OpenTelemetry
Observability

How to Add Observability to a Node.js App with OpenTelemetry

Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
Mar 21, 20266 min read
Next.js
Node.js
BFF

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.

How I Structure CI/CD for Next.js, Docker, and GitHub Actions
Mar 21, 20265 min read
CI/CD
Next.js
Docker

How I Structure CI/CD for Next.js, Docker, and GitHub Actions

A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.