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.