Build Transactional Emails with Resend and React Email in Next.js

Every serious web application needs to send emails — welcome messages, password resets, order confirmations, invoice receipts, and team invitations. Traditionally, building email templates meant wrestling with inline CSS, nested tables, and testing across dozens of email clients. The developer experience was miserable.
React Email changes this by letting you build email templates with React components — the same mental model you use for your UI. Resend provides the delivery infrastructure with a modern API, excellent deliverability, and a generous free tier. Together with Next.js, you get a full-stack email system where templates live alongside your application code, are type-safe, and can be previewed in the browser before sending.
In this tutorial, you will build a complete transactional email system for a SaaS application. You will create templates for welcome emails, password resets, and invoice receipts, preview them in the browser with hot reload, send them through API routes, and deploy everything to production.
Prerequisites
Before starting, make sure you have:
- Node.js 20+ installed
- A Resend account — sign up at the Resend website (free tier includes 3,000 emails/month)
- Basic knowledge of React and TypeScript
- Familiarity with Next.js App Router
- A verified domain (or use Resend's testing domain for development)
What You Will Build
By the end of this tutorial, you will have:
- A Next.js project with React Email integrated
- Three production-ready email templates (welcome, password reset, invoice)
- A local preview server to develop and test emails visually
- API routes to send emails via Resend
- Type-safe email sending utilities with error handling
- A working deployment ready for production
Step 1: Create the Next.js Project
Start by creating a fresh Next.js application:
npx create-next-app@latest saas-emails --typescript --tailwind --eslint --app --src-dir
cd saas-emailsChoose the default options when prompted. This gives you a Next.js 15 project with TypeScript and the App Router.
Step 2: Install Dependencies
Install React Email for template development and Resend for delivery:
npm install resend @react-email/components react-emailThe @react-email/components package provides all the building blocks — Html, Head, Body, Container, Text, Button, Img, Link, Section, Row, Column, Hr, Preview, and more.
Add a script to your package.json for the email preview server:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"email": "email dev --dir src/emails --port 3001"
}
}Step 3: Set Up Environment Variables
Create a .env.local file in your project root:
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxGet your API key from the Resend dashboard under API Keys. For development, you can use the testing API key which sends emails only to your verified email address.
Step 4: Create the Email Directory Structure
Organize your email templates in a dedicated directory:
mkdir -p src/emails/componentsYour project structure will look like this:
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
Step 5: Build Shared Email Components
Before creating full templates, build reusable components that maintain consistent branding across all emails.
Email Header Component
Create 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",
};Email Footer Component
Create 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}>
© 2026 YourApp. All rights reserved.
</Text>
<Text style={footerLinksStyle}>
<Link href="https://yourdomain.com/privacy" style={linkStyle}>
Privacy Policy
</Link>
{" • "}
<Link href="https://yourdomain.com/terms" style={linkStyle}>
Terms of Service
</Link>
{" • "}
<Link href="https://yourdomain.com/unsubscribe" style={linkStyle}>
Unsubscribe
</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",
};Reusable Button Component
Create 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",
};Step 6: Create the Welcome Email Template
Create 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>Welcome to YourApp — let's get you started!</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<EmailHeader title="Welcome aboard!" />
<Section style={contentStyle}>
<Text style={greetingStyle}>Hi {username},</Text>
<Text style={paragraphStyle}>
Thanks for signing up for YourApp! We are excited to have you on
board. Your account is ready and you can start exploring right
away.
</Text>
<Text style={paragraphStyle}>
Here is what you can do next:
</Text>
<Section style={listStyle}>
<Text style={listItemStyle}>
✅ Complete your profile settings
</Text>
<Text style={listItemStyle}>
✅ Create your first project
</Text>
<Text style={listItemStyle}>
✅ Invite your team members
</Text>
<Text style={listItemStyle}>
✅ Explore the documentation
</Text>
</Section>
<Section style={buttonContainerStyle}>
<EmailButton href={loginUrl}>
Get Started
</EmailButton>
</Section>
<Text style={paragraphStyle}>
If you have any questions, reply to this email — we read and
respond to every message.
</Text>
<Text style={signoffStyle}>
Happy building,
<br />
The YourApp Team
</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",
};Step 7: Create the Password Reset Email
Create 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>Reset your YourApp password</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<EmailHeader title="Password Reset" />
<Section style={contentStyle}>
<Text style={paragraphStyle}>Hi {username},</Text>
<Text style={paragraphStyle}>
We received a request to reset your password. Click the button
below to choose a new password:
</Text>
<Section style={buttonContainerStyle}>
<EmailButton href={resetUrl}>Reset Password</EmailButton>
</Section>
<Section style={warningBoxStyle}>
<Text style={warningTextStyle}>
⏰ This link expires in {expiresInMinutes} minutes. If you did
not request a password reset, you can safely ignore this
email.
</Text>
</Section>
<Text style={paragraphStyle}>
If the button does not work, copy and paste this URL into your
browser:
</Text>
<Section style={urlBoxStyle}>
<Code style={urlTextStyle}>{resetUrl}</Code>
</Section>
<Text style={securityNoteStyle}>
For security, this request was received from a web browser. If
you did not make this request, please change your password
immediately.
</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",
};Step 8: Create the Invoice Email
Create 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("en-US", {
style: "currency",
currency,
}).format(amount);
}
export default function InvoiceEmail({
customerName = "John Doe",
invoiceNumber = "INV-2026-001",
invoiceDate = "March 15, 2026",
dueDate = "April 15, 2026",
items = [
{ description: "Pro Plan - Monthly", quantity: 1, unitPrice: 29 },
{ description: "Extra Seats (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>
Invoice {invoiceNumber} — {formatCurrency(total, currency)} due{" "}
{dueDate}
</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<EmailHeader title="Invoice" />
<Section style={contentStyle}>
<Text style={paragraphStyle}>Hi {customerName},</Text>
<Text style={paragraphStyle}>
Here is your invoice. Please find the details below:
</Text>
{/* Invoice Meta */}
<Section style={metaBoxStyle}>
<Row>
<Column>
<Text style={metaLabelStyle}>Invoice Number</Text>
<Text style={metaValueStyle}>{invoiceNumber}</Text>
</Column>
<Column>
<Text style={metaLabelStyle}>Date</Text>
<Text style={metaValueStyle}>{invoiceDate}</Text>
</Column>
<Column>
<Text style={metaLabelStyle}>Due Date</Text>
<Text style={metaValueStyle}>{dueDate}</Text>
</Column>
</Row>
</Section>
{/* Line Items Header */}
<Section style={tableHeaderStyle}>
<Row>
<Column style={descColStyle}>
<Text style={headerCellStyle}>Description</Text>
</Column>
<Column style={qtyColStyle}>
<Text style={headerCellStyle}>Qty</Text>
</Column>
<Column style={priceColStyle}>
<Text style={headerCellStyle}>Price</Text>
</Column>
<Column style={totalColStyle}>
<Text style={headerCellStyle}>Total</Text>
</Column>
</Row>
</Section>
{/* Line Items */}
{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>
))}
{/* Totals */}
<Hr style={dividerStyle} />
<Section style={totalsStyle}>
<Row>
<Column style={totalsLabelColStyle}>
<Text style={totalsLabelStyle}>Subtotal</Text>
</Column>
<Column style={totalsValueColStyle}>
<Text style={totalsValueStyle}>
{formatCurrency(subtotal, currency)}
</Text>
</Column>
</Row>
<Row>
<Column style={totalsLabelColStyle}>
<Text style={totalsLabelStyle}>Tax (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 Due</Text>
</Column>
<Column style={totalsValueColStyle}>
<Text style={grandTotalValueStyle}>
{formatCurrency(total, currency)}
</Text>
</Column>
</Row>
</Section>
<Section style={buttonContainerStyle}>
<EmailButton href={invoiceUrl}>
View Invoice Online
</EmailButton>
</Section>
<Text style={noteStyle}>
Payment is due by {dueDate}. If you have already sent payment,
please disregard this 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",
};Step 9: Preview Your Emails Locally
Run the React Email preview server:
npm run emailOpen http://localhost:3001 in your browser. You will see all three email templates listed. Click on any template to see a live preview. Changes to your template files will hot-reload instantly.
The preview server lets you:
- View the rendered email exactly as it will appear in email clients
- Toggle between desktop and mobile viewports
- Copy the HTML source for testing in other tools
- Send a test email directly from the preview interface
- Switch between props to test different states
This is one of the biggest advantages of React Email — you develop emails with the same workflow as your UI components.
Step 10: Set Up the Resend Client
Create src/lib/resend.ts:
import { Resend } from "resend";
if (!process.env.RESEND_API_KEY) {
throw new Error("RESEND_API_KEY environment variable is not set");
}
export const resend = new Resend(process.env.RESEND_API_KEY);
// Default sender — use your verified domain in production
export const FROM_EMAIL = "YourApp <noreply@yourdomain.com>";Step 11: Create the Email Sending API Route
Create 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: "Missing required fields: template, to" },
{ status: 400 }
);
}
const emailConfig = getEmailConfig(template, data);
if (!emailConfig) {
return NextResponse.json(
{ error: `Unknown template: ${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("Resend error:", error);
return NextResponse.json(
{ error: "Failed to send email" },
{ status: 500 }
);
}
return NextResponse.json({ success: true, id: result?.id });
} catch (err) {
console.error("Email send error:", err);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
function getEmailConfig(
template: EmailTemplate,
data: Record<string, unknown>
) {
switch (template) {
case "welcome":
return {
subject: "Welcome to YourApp!",
component: WelcomeEmail({
username: (data.username as string) || "there",
loginUrl: data.loginUrl as string,
}),
};
case "password-reset":
return {
subject: "Reset your password",
component: PasswordResetEmail({
username: (data.username as string) || "there",
resetUrl: data.resetUrl as string,
expiresInMinutes: (data.expiresInMinutes as number) || 60,
}),
};
case "invoice":
return {
subject: `Invoice ${data.invoiceNumber || ""}`,
component: InvoiceEmail({
customerName: (data.customerName as string) || "Customer",
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;
}
}Step 12: Create a Type-Safe Email Utility
For a cleaner developer experience, create a utility that provides type safety when sending emails. Create 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: "Welcome to YourApp!",
render: (data: WelcomeEmailData) => WelcomeEmail(data),
},
"password-reset": {
subject: "Reset your password",
render: (data: PasswordResetData) => PasswordResetEmail(data),
},
invoice: {
subject: (data: InvoiceData) => `Invoice ${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(`Failed to send ${template} email: ${error.message}`);
}
return result;
}Now you can use it anywhere in your server code with full type safety:
// In a Server Action or API route
import { sendEmail } from "@/lib/send-email";
// TypeScript knows exactly what data each template needs
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", "billing@company.com", {
customerName: "Acme Corp",
invoiceNumber: "INV-2026-042",
invoiceDate: "March 15, 2026",
dueDate: "April 15, 2026",
items: [
{ description: "Pro Plan", quantity: 1, unitPrice: 29 },
],
});Step 13: Use Emails in Server Actions
Create a Server Action that sends a welcome email when a user signs up. Create 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;
// ... your user creation logic here ...
// Send welcome email
try {
await sendEmail("welcome", email, {
username: name,
loginUrl: `https://yourdomain.com/login`,
});
} catch (error) {
// Log but don't fail the signup if email fails
console.error("Failed to send welcome email:", error);
}
return { success: true };
}Step 14: Test Email Delivery
Test your API route using curl:
curl -X POST http://localhost:3000/api/email/send \
-H "Content-Type: application/json" \
-d '{
"template": "welcome",
"to": "your-email@example.com",
"data": {
"username": "Test User"
}
}'You should receive the welcome email in your inbox. Check the Resend dashboard to see delivery status, open rates, and bounce information.
Step 15: Add Email Webhooks for Delivery Tracking
Resend provides webhooks for tracking email events. Create 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} delivered to ${body.data.to}`);
// Update your database
break;
case "email.bounced":
console.log(`Email ${body.data.email_id} bounced`);
// Flag the email address as invalid
break;
case "email.complained":
console.log(`Spam complaint for ${body.data.email_id}`);
// Unsubscribe the user immediately
break;
case "email.opened":
console.log(`Email ${body.data.email_id} opened`);
// Track engagement
break;
}
return NextResponse.json({ received: true });
}Register this webhook URL in your Resend dashboard under Webhooks.
Troubleshooting
Common issues and solutions:
Emails not arriving in inbox:
- Check your Resend dashboard for delivery status
- Verify your domain DNS records (SPF, DKIM, DMARC)
- During development, use the Resend testing domain which only sends to verified addresses
React Email preview not loading:
- Make sure port 3001 is not in use
- Check that your email files export a default component
- Verify all imports from
@react-email/componentsare correct
TypeScript errors in email templates:
- Email templates use inline styles (not Tailwind) — use
React.CSSPropertiestypes - The
styleprop on React Email components expects specific CSS property names
Emails look different across clients:
- Always test with multiple clients (Gmail, Outlook, Apple Mail)
- Stick to tables for complex layouts — some clients do not support flexbox or grid
- Use inline styles — external CSS is stripped by most email clients
- Keep images under 600px wide
Next Steps
- Add batch sending with
resend.batch.send()for newsletters - Implement email scheduling with
scheduledAtparameter - Set up A/B testing for subject lines using Resend's built-in support
- Add unsubscribe handling with one-click unsubscribe headers
- Explore Resend Audiences for managing subscriber lists
- Build a notification preferences page to let users control which emails they receive
Conclusion
You have built a complete transactional email system using React Email, Resend, and Next.js. Your emails are built with React components, type-safe, previewable in the browser during development, and delivered reliably through Resend's infrastructure.
The combination of React Email's component model and Resend's modern delivery API gives you the best developer experience for email in the JavaScript ecosystem. Templates are just React components — you can compose them, share props, and test them exactly like your UI code. No more inline table hacks or copy-pasting HTML between tools.
As your application grows, this foundation scales with you — add new templates by creating new React components, extend the sendEmail utility with new template types, and monitor everything through the Resend dashboard.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Build End-to-End Type-Safe APIs with tRPC and Next.js App Router
Learn how to build fully type-safe APIs with tRPC and Next.js 15 App Router. This hands-on tutorial covers router setup, procedures, middleware, React Query integration, and server-side calls — all without writing a single API schema.

Building an Autonomous AI Agent with Agentic RAG and Next.js
Learn how to build an AI agent that autonomously decides when and how to retrieve information from vector databases. A comprehensive hands-on guide using Vercel AI SDK and Next.js with executable examples.

Better Auth with Next.js 15: The Complete Authentication Guide for 2026
Learn how to implement full-featured authentication in Next.js 15 using Better Auth. This tutorial covers email/password, OAuth, sessions, middleware protection, and role-based access control.