Back to Blog
EmailTransactional EmailBackendBest Practices

Email Sending Best Practices for Applications

Send emails that get delivered. From transactional emails to templates to deliverability optimization.

B
Bootspring Team
Engineering
October 12, 2023
6 min read

Emails are critical for user engagement—password resets, order confirmations, notifications. Poor email practices lead to spam folders and frustrated users. Here's how to do it right.

Email Service Setup#

1import { Resend } from 'resend'; 2 3const resend = new Resend(process.env.RESEND_API_KEY); 4 5// Basic email sending 6async function sendEmail({ 7 to, 8 subject, 9 html, 10 text, 11 from = 'noreply@example.com', 12}: EmailParams): Promise<void> { 13 try { 14 const result = await resend.emails.send({ 15 from: `Your App <${from}>`, 16 to, 17 subject, 18 html, 19 text, 20 }); 21 22 logger.info('Email sent', { 23 to, 24 subject, 25 messageId: result.id, 26 }); 27 } catch (error) { 28 logger.error('Email failed', { 29 to, 30 subject, 31 error: error.message, 32 }); 33 throw error; 34 } 35}

Email Templates#

1// React Email templates 2import { Html, Head, Body, Container, Text, Button, Img } from '@react-email/components'; 3 4interface WelcomeEmailProps { 5 name: string; 6 verificationUrl: string; 7} 8 9export function WelcomeEmail({ name, verificationUrl }: WelcomeEmailProps) { 10 return ( 11 <Html> 12 <Head /> 13 <Body style={bodyStyle}> 14 <Container style={containerStyle}> 15 <Img 16 src="https://example.com/logo.png" 17 width="120" 18 height="40" 19 alt="Logo" 20 /> 21 22 <Text style={headingStyle}>Welcome, {name}!</Text> 23 24 <Text style={textStyle}> 25 Thanks for signing up. Please verify your email address to get started. 26 </Text> 27 28 <Button style={buttonStyle} href={verificationUrl}> 29 Verify Email 30 </Button> 31 32 <Text style={footerStyle}> 33 If you didn't create an account, you can safely ignore this email. 34 </Text> 35 </Container> 36 </Body> 37 </Html> 38 ); 39} 40 41const bodyStyle = { 42 backgroundColor: '#f6f9fc', 43 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', 44}; 45 46const containerStyle = { 47 backgroundColor: '#ffffff', 48 margin: '40px auto', 49 padding: '40px', 50 borderRadius: '8px', 51 maxWidth: '600px', 52}; 53 54const headingStyle = { 55 fontSize: '24px', 56 fontWeight: 'bold', 57 marginBottom: '20px', 58}; 59 60const textStyle = { 61 fontSize: '16px', 62 lineHeight: '24px', 63 color: '#525252', 64}; 65 66const buttonStyle = { 67 backgroundColor: '#3b82f6', 68 color: '#ffffff', 69 padding: '12px 24px', 70 borderRadius: '6px', 71 textDecoration: 'none', 72 display: 'inline-block', 73 marginTop: '20px', 74}; 75 76const footerStyle = { 77 fontSize: '14px', 78 color: '#8c8c8c', 79 marginTop: '40px', 80};

Email Queue System#

1import { Queue, Worker } from 'bullmq'; 2 3const emailQueue = new Queue('emails', { 4 connection: redis, 5 defaultJobOptions: { 6 attempts: 3, 7 backoff: { 8 type: 'exponential', 9 delay: 60000, // Start with 1 minute 10 }, 11 removeOnComplete: 100, 12 removeOnFail: 1000, 13 }, 14}); 15 16// Queue email 17async function queueEmail(params: EmailParams): Promise<void> { 18 await emailQueue.add('send-email', params, { 19 priority: getPriority(params.type), 20 }); 21} 22 23function getPriority(type: string): number { 24 const priorities: Record<string, number> = { 25 'password-reset': 1, 26 'order-confirmation': 2, 27 'welcome': 3, 28 'newsletter': 10, 29 }; 30 return priorities[type] || 5; 31} 32 33// Worker 34const emailWorker = new Worker( 35 'emails', 36 async (job) => { 37 const { to, subject, template, data } = job.data; 38 39 // Render template 40 const html = await renderTemplate(template, data); 41 const text = htmlToText(html); 42 43 // Send 44 await sendEmail({ to, subject, html, text }); 45 }, 46 { 47 connection: redis, 48 concurrency: 10, 49 limiter: { 50 max: 100, 51 duration: 1000, // 100 emails per second 52 }, 53 } 54);

Transactional Emails#

1// Email service with template management 2class EmailService { 3 private templates = new Map<string, EmailTemplate>(); 4 5 async send(type: string, to: string, data: Record<string, any>): Promise<void> { 6 const template = this.templates.get(type); 7 if (!template) { 8 throw new Error(`Unknown email template: ${type}`); 9 } 10 11 await queueEmail({ 12 to, 13 subject: this.interpolate(template.subject, data), 14 template: type, 15 data, 16 type, 17 }); 18 } 19 20 // Password reset 21 async sendPasswordReset(email: string, token: string): Promise<void> { 22 const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`; 23 24 await this.send('password-reset', email, { 25 resetUrl, 26 expiresIn: '1 hour', 27 }); 28 } 29 30 // Order confirmation 31 async sendOrderConfirmation(order: Order): Promise<void> { 32 await this.send('order-confirmation', order.customer.email, { 33 orderNumber: order.number, 34 items: order.items, 35 total: formatCurrency(order.total), 36 estimatedDelivery: order.estimatedDelivery, 37 }); 38 } 39 40 // Welcome email with verification 41 async sendWelcome(user: User, verificationToken: string): Promise<void> { 42 const verificationUrl = `${process.env.APP_URL}/verify?token=${verificationToken}`; 43 44 await this.send('welcome', user.email, { 45 name: user.name, 46 verificationUrl, 47 }); 48 } 49 50 private interpolate(template: string, data: Record<string, any>): string { 51 return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] || ''); 52 } 53}

Deliverability#

1// SPF, DKIM, DMARC configuration 2const dnsRecords = { 3 // SPF 4 spf: { 5 type: 'TXT', 6 name: '@', 7 value: 'v=spf1 include:_spf.resend.com ~all', 8 }, 9 10 // DKIM 11 dkim: { 12 type: 'CNAME', 13 name: 'resend._domainkey', 14 value: 'resend.domainkey.resend.dev', 15 }, 16 17 // DMARC 18 dmarc: { 19 type: 'TXT', 20 name: '_dmarc', 21 value: 'v=DMARC1; p=none; rua=mailto:dmarc@example.com', 22 }, 23}; 24 25// Validate email before sending 26async function validateEmail(email: string): Promise<boolean> { 27 // Basic format check 28 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 29 if (!emailRegex.test(email)) { 30 return false; 31 } 32 33 // Check for disposable email domains 34 const domain = email.split('@')[1]; 35 const isDisposable = await checkDisposableDomain(domain); 36 if (isDisposable) { 37 return false; 38 } 39 40 // MX record check 41 const hasMx = await checkMxRecord(domain); 42 return hasMx; 43}

Unsubscribe Handling#

1// One-click unsubscribe (RFC 8058) 2function getUnsubscribeHeaders(userId: string, listId: string) { 3 const unsubscribeUrl = `${process.env.APP_URL}/unsubscribe?user=${userId}&list=${listId}`; 4 const unsubscribeEmail = `unsubscribe+${userId}@example.com`; 5 6 return { 7 'List-Unsubscribe': `<${unsubscribeUrl}>, <mailto:${unsubscribeEmail}>`, 8 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click', 9 }; 10} 11 12// Handle unsubscribe 13app.post('/unsubscribe', async (req, res) => { 14 const { user, list } = req.query; 15 16 await prisma.emailPreference.update({ 17 where: { userId_listId: { userId: user, listId: list } }, 18 data: { subscribed: false }, 19 }); 20 21 res.send('You have been unsubscribed.'); 22}); 23 24// Check preferences before sending 25async function canSendEmail(userId: string, type: string): Promise<boolean> { 26 const prefs = await prisma.emailPreference.findUnique({ 27 where: { userId_type: { userId, type } }, 28 }); 29 30 return prefs?.subscribed !== false; 31}

Email Analytics#

1// Track opens and clicks 2async function trackEmailOpen(emailId: string): Promise<void> { 3 await prisma.emailEvent.create({ 4 data: { 5 emailId, 6 event: 'open', 7 timestamp: new Date(), 8 }, 9 }); 10} 11 12// Tracking pixel 13app.get('/track/open/:emailId', async (req, res) => { 14 await trackEmailOpen(req.params.emailId); 15 16 // Return 1x1 transparent pixel 17 const pixel = Buffer.from( 18 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 19 'base64' 20 ); 21 22 res.setHeader('Content-Type', 'image/gif'); 23 res.send(pixel); 24}); 25 26// Click tracking 27app.get('/track/click/:emailId', async (req, res) => { 28 const { url } = req.query; 29 30 await prisma.emailEvent.create({ 31 data: { 32 emailId: req.params.emailId, 33 event: 'click', 34 url: url as string, 35 timestamp: new Date(), 36 }, 37 }); 38 39 res.redirect(url as string); 40});

Best Practices#

Content: ✓ Clear, specific subject lines ✓ Personalize when possible ✓ Include plain text version ✓ Keep images small ✓ Test on multiple clients Technical: ✓ Set up SPF, DKIM, DMARC ✓ Use dedicated sending domain ✓ Warm up new IP addresses ✓ Handle bounces and complaints ✓ Respect rate limits Compliance: ✓ Include unsubscribe link ✓ Honor unsubscribe requests ✓ Include physical address ✓ Don't buy email lists ✓ Get explicit consent

Conclusion#

Email is a critical communication channel. Use a reliable service, queue emails for resilience, and focus on deliverability with proper DNS configuration.

Always provide easy unsubscribe options and respect user preferences—it's both good practice and legally required.

Share this article

Help spread the word about Bootspring