Transactional emails are critical for user onboarding, notifications, and receipts. Here is how to build a robust email system.
Setup
pnpm add resend @react-email/components
// lib/email.ts
import { Resend } from "resend";
export const resend = new Resend(process.env.RESEND_API_KEY);
export const FROM_EMAIL = "notifications@yourdomain.com";
export const REPLY_TO = "support@yourdomain.com";
Base Email Layout
// emails/BaseLayout.tsx
import {
Body,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Section,
Text,
} from "@react-email/components";
interface BaseLayoutProps {
preview: string;
children: React.ReactNode;
}
export function BaseLayout({ preview, children }: BaseLayoutProps) {
return (
<Html>
<Head />
<Preview>{preview}</Preview>
<Body style={body}>
<Container style={container}>
<Section style={header}>
<Img
src="https://yourdomain.com/logo.png"
width={120}
height={32}
alt="Company"
/>
</Section>
{children}
<Hr style={hr} />
<Section style={footer}>
<Text style={footerText}>
Your Company, 123 Main St, City, State 12345
</Text>
<Text style={footerText}>
<Link href="https://yourdomain.com/unsubscribe" style={link}>
Unsubscribe
</Link>
{" | "}
<Link href="https://yourdomain.com/preferences" style={link}>
Email preferences
</Link>
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
const body = { backgroundColor: "#f6f9fc", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" };
const container = { backgroundColor: "#ffffff", margin: "0 auto", padding: "20px 0 48px", maxWidth: "580px" };
const header = { padding: "20px 32px" };
const hr = { borderColor: "#e6ebf1", margin: "20px 0" };
const footer = { padding: "0 32px" };
const footerText = { color: "#8898aa", fontSize: "12px", lineHeight: "16px" };
const link = { color: "#556cd6", textDecoration: "underline" };
Welcome Email Template
// emails/WelcomeEmail.tsx
import { Button, Heading, Section, Text } from "@react-email/components";
import { BaseLayout } from "./BaseLayout";
interface WelcomeEmailProps {
name: string;
loginUrl: string;
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<BaseLayout preview={`Welcome to our platform, ${name}`}>
<Section style={{ padding: "0 32px" }}>
<Heading as="h1" style={{ fontSize: "24px", marginBottom: "16px" }}>
Welcome, {name}!
</Heading>
<Text style={{ fontSize: "16px", lineHeight: "24px", color: "#525f7f" }}>
Thank you for signing up. Your account is ready and waiting for you.
</Text>
<Text style={{ fontSize: "16px", lineHeight: "24px", color: "#525f7f" }}>
Here is what you can do next:
</Text>
<ul>
<li>Complete your profile</li>
<li>Create your first project</li>
<li>Invite team members</li>
</ul>
<Button
href={loginUrl}
style={{
backgroundColor: "#556cd6",
borderRadius: "5px",
color: "#fff",
fontSize: "16px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 20px",
marginTop: "16px",
}}
>
Go to dashboard
</Button>
</Section>
</BaseLayout>
);
}
Password Reset Template
// emails/PasswordResetEmail.tsx
import { Button, Heading, Section, Text } from "@react-email/components";
import { BaseLayout } from "./BaseLayout";
interface PasswordResetEmailProps {
name: string;
resetUrl: string;
expiresIn: string;
}
export function PasswordResetEmail({
name,
resetUrl,
expiresIn,
}: PasswordResetEmailProps) {
return (
<BaseLayout preview="Reset your password">
<Section style={{ padding: "0 32px" }}>
<Heading as="h1" style={{ fontSize: "24px", marginBottom: "16px" }}>
Password reset
</Heading>
<Text style={{ fontSize: "16px", lineHeight: "24px", color: "#525f7f" }}>
Hi {name}, we received a request to reset your password. Click the
button below to set a new one. This link expires in {expiresIn}.
</Text>
<Button
href={resetUrl}
style={{
backgroundColor: "#556cd6",
borderRadius: "5px",
color: "#fff",
fontSize: "16px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 20px",
}}
>
Reset password
</Button>
<Text style={{ fontSize: "14px", color: "#8898aa", marginTop: "16px" }}>
If you did not request this, you can safely ignore this email.
</Text>
</Section>
</BaseLayout>
);
}
Email Service
// lib/email-service.ts
import { resend, FROM_EMAIL, REPLY_TO } from "./email";
import { WelcomeEmail } from "@/emails/WelcomeEmail";
import { PasswordResetEmail } from "@/emails/PasswordResetEmail";
import { createElement } from "react";
type EmailType = "welcome" | "password-reset" | "invoice" | "notification";
interface SendEmailOptions {
to: string | string[];
subject: string;
react: React.ReactElement;
tags?: { name: string; value: string }[];
}
async function sendEmail({ to, subject, react, tags }: SendEmailOptions) {
const { data, error } = await resend.emails.send({
from: FROM_EMAIL,
to: Array.isArray(to) ? to : [to],
replyTo: REPLY_TO,
subject,
react,
tags,
});
if (error) {
console.error("Email send failed:", error);
throw new Error(`Failed to send email: ${error.message}`);
}
return data;
}
export async function sendWelcomeEmail(email: string, name: string) {
return sendEmail({
to: email,
subject: `Welcome to our platform, ${name}!`,
react: createElement(WelcomeEmail, {
name,
loginUrl: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
}),
tags: [{ name: "type", value: "welcome" }],
});
}
export async function sendPasswordResetEmail(
email: string,
name: string,
token: string
) {
return sendEmail({
to: email,
subject: "Reset your password",
react: createElement(PasswordResetEmail, {
name,
resetUrl: `${process.env.NEXT_PUBLIC_URL}/reset-password?token=${token}`,
expiresIn: "1 hour",
}),
tags: [{ name: "type", value: "password-reset" }],
});
}
// Batch send
export async function sendBatchEmails(
emails: { to: string; subject: string; react: React.ReactElement }[]
) {
const batch = emails.map((e) => ({
from: FROM_EMAIL,
to: e.to,
subject: e.subject,
react: e.react,
}));
const { data, error } = await resend.batch.send(batch);
if (error) throw new Error(`Batch send failed: ${error.message}`);
return data;
}
API Route Usage
// app/api/auth/register/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sendWelcomeEmail } from "@/lib/email-service";
export async function POST(request: NextRequest) {
const { email, name, password } = await request.json();
// ... create user in database ...
// Send welcome email (non-blocking)
sendWelcomeEmail(email, name).catch((err) =>
console.error("Welcome email failed:", err)
);
return NextResponse.json({ success: true });
}
Preview Emails in Development
pnpm add -D email-dev
{
"scripts": {
"email:dev": "email dev --dir emails --port 3030"
}
}
Run pnpm email:dev to preview your templates at http://localhost:3030.
Need Transactional Email Integration?
We build email systems that deliver reliably and look great in every inbox. Contact us to discuss your email needs.