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 WelcomeEmail

Password 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 PasswordResetEmail

Email 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#

  1. Install dependencies: npm install resend @react-email/components
  2. Configure Resend: Add RESEND_API_KEY to environment variables
  3. Create templates: Build React Email components in emails/ directory
  4. Set up service: Create email service with typed methods
  5. Send emails: Call service methods from server actions or API routes
  6. Preview locally: Run npx email dev for hot-reloading previews

Best Practices#

  1. Use React Email - Type-safe, component-based email templates
  2. Always include plain text - Generate or provide plain text fallbacks
  3. Tag emails - Use tags for analytics and filtering in Resend
  4. Handle errors gracefully - Log failures and notify admins
  5. Preview before sending - Use the email dev server during development
  6. Test with real clients - Verify rendering in Gmail, Outlook, etc.
  7. Set up domain verification - Improve deliverability with SPF/DKIM
  8. Use consistent branding - Create shared style constants