Transactional Email Pattern
Send reliable, well-designed transactional emails using Resend and React Email components.
Overview#
Transactional emails are automated messages triggered by user actions - welcome emails, password resets, order confirmations, and notifications. This pattern covers building a robust email system with React Email for templates and Resend for delivery.
When to use:
- Welcome and onboarding emails
- Password reset flows
- Order and payment confirmations
- Account notifications
- Activity alerts
Key features:
- Resend integration for delivery
- React Email for templates
- Plain text fallback generation
- Email tagging for analytics
- Error handling
Code Example#
Email Service Setup#
1// lib/email/index.ts
2import { Resend } from 'resend'
3
4export const resend = new Resend(process.env.RESEND_API_KEY)
5
6interface SendEmailOptions {
7 to: string | string[]
8 subject: string
9 html: string
10 text?: string
11 from?: string
12 replyTo?: string
13 tags?: { name: string; value: string }[]
14}
15
16export async function sendEmail(options: SendEmailOptions) {
17 const { to, subject, html, text, from, replyTo, tags } = options
18
19 const result = await resend.emails.send({
20 from: from ?? `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
21 to: Array.isArray(to) ? to : [to],
22 subject,
23 html,
24 text: text ?? stripHtml(html),
25 reply_to: replyTo,
26 tags
27 })
28
29 if (result.error) {
30 console.error('Email send failed:', result.error)
31 throw new Error(`Failed to send email: ${result.error.message}`)
32 }
33
34 return result.data
35}
36
37function stripHtml(html: string): string {
38 return html.replace(/<[^>]*>/g, '').trim()
39}Welcome Email Template#
1// emails/WelcomeEmail.tsx
2import {
3 Body,
4 Button,
5 Container,
6 Head,
7 Heading,
8 Html,
9 Img,
10 Preview,
11 Section,
12 Text
13} from '@react-email/components'
14
15interface WelcomeEmailProps {
16 name: string
17 loginUrl: string
18}
19
20export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
21 return (
22 <Html>
23 <Head />
24 <Preview>Welcome to our platform!</Preview>
25 <Body style={main}>
26 <Container style={container}>
27 <Img
28 src={`${process.env.NEXT_PUBLIC_APP_URL}/logo.png`}
29 width="150"
30 height="50"
31 alt="Logo"
32 />
33
34 <Heading style={heading}>Welcome, {name}!</Heading>
35
36 <Text style={text}>
37 Thank you for joining us. We are excited to have you on board.
38 </Text>
39
40 <Section style={buttonContainer}>
41 <Button style={button} href={loginUrl}>
42 Get Started
43 </Button>
44 </Section>
45
46 <Text style={footer}>
47 If you did not create this account, please ignore this email.
48 </Text>
49 </Container>
50 </Body>
51 </Html>
52 )
53}
54
55const main = {
56 backgroundColor: '#f6f9fc',
57 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
58}
59
60const container = {
61 backgroundColor: '#ffffff',
62 margin: '0 auto',
63 padding: '40px 20px',
64 maxWidth: '600px'
65}
66
67const heading = {
68 fontSize: '24px',
69 fontWeight: '600',
70 color: '#1a1a1a',
71 margin: '30px 0'
72}
73
74const text = {
75 fontSize: '16px',
76 color: '#4a4a4a',
77 lineHeight: '26px'
78}
79
80const buttonContainer = {
81 textAlign: 'center' as const,
82 margin: '30px 0'
83}
84
85const button = {
86 backgroundColor: '#2563eb',
87 borderRadius: '6px',
88 color: '#ffffff',
89 fontSize: '16px',
90 fontWeight: '600',
91 padding: '12px 24px',
92 textDecoration: 'none'
93}
94
95const footer = {
96 fontSize: '14px',
97 color: '#8a8a8a',
98 marginTop: '40px'
99}
100
101export default WelcomeEmailPassword Reset Email#
1// emails/PasswordResetEmail.tsx
2import {
3 Body,
4 Button,
5 Container,
6 Head,
7 Heading,
8 Html,
9 Preview,
10 Section,
11 Text
12} from '@react-email/components'
13
14interface PasswordResetEmailProps {
15 name: string
16 resetUrl: string
17 expiresIn: string
18}
19
20export function PasswordResetEmail({
21 name,
22 resetUrl,
23 expiresIn
24}: PasswordResetEmailProps) {
25 return (
26 <Html>
27 <Head />
28 <Preview>Reset your password</Preview>
29 <Body style={main}>
30 <Container style={container}>
31 <Heading style={heading}>Reset Your Password</Heading>
32
33 <Text style={text}>Hi {name},</Text>
34
35 <Text style={text}>
36 We received a request to reset your password. Click the button
37 below to choose a new password.
38 </Text>
39
40 <Section style={buttonContainer}>
41 <Button style={button} href={resetUrl}>
42 Reset Password
43 </Button>
44 </Section>
45
46 <Text style={text}>
47 This link will expire in {expiresIn}. If you did not request
48 a password reset, you can safely ignore this email.
49 </Text>
50
51 <Text style={smallText}>
52 If the button does not work, copy and paste this URL into your browser:
53 <br />
54 {resetUrl}
55 </Text>
56 </Container>
57 </Body>
58 </Html>
59 )
60}
61
62const smallText = {
63 fontSize: '12px',
64 color: '#8a8a8a',
65 marginTop: '30px'
66}
67
68// ... other styles same as WelcomeEmail
69
70export default PasswordResetEmailEmail Sending Service#
1// lib/email/service.ts
2import { render } from '@react-email/render'
3import { sendEmail } from '.'
4import WelcomeEmail from '@/emails/WelcomeEmail'
5import PasswordResetEmail from '@/emails/PasswordResetEmail'
6
7export const emailService = {
8 async sendWelcome(to: string, name: string) {
9 const loginUrl = `${process.env.NEXT_PUBLIC_APP_URL}/login`
10
11 const html = await render(WelcomeEmail({ name, loginUrl }))
12
13 return sendEmail({
14 to,
15 subject: 'Welcome to Our Platform!',
16 html,
17 tags: [{ name: 'category', value: 'welcome' }]
18 })
19 },
20
21 async sendPasswordReset(to: string, name: string, token: string) {
22 const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`
23
24 const html = await render(
25 PasswordResetEmail({ name, resetUrl, expiresIn: '1 hour' })
26 )
27
28 return sendEmail({
29 to,
30 subject: 'Reset Your Password',
31 html,
32 tags: [{ name: 'category', value: 'password-reset' }]
33 })
34 }
35}Usage in Server Actions#
1// actions/auth.ts
2'use server'
3
4import { prisma } from '@/lib/db'
5import { emailService } from '@/lib/email/service'
6import { generateToken } from '@/lib/tokens'
7
8export async function requestPasswordReset(email: string) {
9 const user = await prisma.user.findUnique({
10 where: { email }
11 })
12
13 if (!user) {
14 // Return success even if user not found (security)
15 return { success: true }
16 }
17
18 const token = await generateToken()
19
20 await prisma.passwordResetToken.create({
21 data: {
22 userId: user.id,
23 token,
24 expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour
25 }
26 })
27
28 await emailService.sendPasswordReset(
29 user.email,
30 user.name ?? 'User',
31 token
32 )
33
34 return { success: true }
35}Email Preview Route (Development)#
1// app/api/email-preview/[template]/route.ts
2import { NextRequest, NextResponse } from 'next/server'
3import { render } from '@react-email/render'
4import WelcomeEmail from '@/emails/WelcomeEmail'
5import PasswordResetEmail from '@/emails/PasswordResetEmail'
6
7const templates: Record<string, React.ComponentType<any>> = {
8 welcome: WelcomeEmail,
9 'password-reset': PasswordResetEmail
10}
11
12const mockData: Record<string, any> = {
13 welcome: {
14 name: 'John Doe',
15 loginUrl: 'https://example.com/login'
16 },
17 'password-reset': {
18 name: 'John Doe',
19 resetUrl: 'https://example.com/reset?token=abc123',
20 expiresIn: '1 hour'
21 }
22}
23
24export async function GET(
25 request: NextRequest,
26 { params }: { params: { template: string } }
27) {
28 // Only allow in development
29 if (process.env.NODE_ENV !== 'development') {
30 return NextResponse.json({ error: 'Not found' }, { status: 404 })
31 }
32
33 const Template = templates[params.template]
34 const data = mockData[params.template]
35
36 if (!Template || !data) {
37 return NextResponse.json({ error: 'Template not found' }, { status: 404 })
38 }
39
40 const html = await render(Template(data))
41
42 return new NextResponse(html, {
43 headers: { 'Content-Type': 'text/html' }
44 })
45}Usage Instructions#
- Install dependencies:
npm install resend @react-email/components - Configure Resend: Add
RESEND_API_KEYto environment variables - Create templates: Build React Email components in
emails/directory - Set up service: Create email service with typed methods
- Send emails: Call service methods from server actions or API routes
- Preview locally: Run
npx email devfor hot-reloading previews
Best Practices#
- Use React Email - Type-safe, component-based email templates
- Always include plain text - Generate or provide plain text fallbacks
- Tag emails - Use tags for analytics and filtering in Resend
- Handle errors gracefully - Log failures and notify admins
- Preview before sending - Use the email dev server during development
- Test with real clients - Verify rendering in Gmail, Outlook, etc.
- Set up domain verification - Improve deliverability with SPF/DKIM
- Use consistent branding - Create shared style constants
Related Patterns#
- Email Templates - Reusable template components
- Email Queues - Background email processing
- Email Tracking - Open and click tracking