Email Service Integration
Phase 3: Email Service Integration
Overview
Integrate an email service to send verification emails. This phase covers setup for multiple providers and email template design.
Requirements
Functional:
- β’Send verification emails reliably
- β’Include verification link with token
- β’Support email templates
- β’Handle send failures gracefully
Non-functional:
- β’Use transactional email service (not SMTP relay)
- β’Track delivery status
- β’HTML + plain text fallback
Provider Options
| Provider | Pros | Cons | Free Tier |
|---|---|---|---|
| Resend | Simple API, React Email | Newer | 3,000/month |
| SendGrid | Mature, good analytics | Complex setup | 100/day |
| Postmark | Fast delivery, great DX | No marketing email | 100/month |
| AWS SES | Cheap at scale | Requires AWS setup | 62,000/month (EC2) |
Implementation
Option A: Resend (Recommended for Next.js)
npm install resend
// src/lib/email/resend.ts
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
interface SendVerificationEmailOptions {
to: string
verifyUrl: string
userName?: string
}
export async function sendVerificationEmail(options: SendVerificationEmailOptions): Promise<{ success: boolean; error?: string }> {
const { to, verifyUrl, userName } = options
try {
const { error } = await resend.emails.send({
from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
to,
subject: 'Verify your email address',
html: getVerificationEmailHtml({ verifyUrl, userName }),
text: getVerificationEmailText({ verifyUrl, userName }),
})
if (error) {
console.error('Email send error:', error)
return { success: false, error: error.message }
}
return { success: true }
} catch (err) {
console.error('Email service error:', err)
return { success: false, error: 'Failed to send email' }
}
}
Option B: SendGrid
npm install @sendgrid/mail
// src/lib/email/sendgrid.ts
import sgMail from '@sendgrid/mail'
sgMail.setApiKey(process.env.SENDGRID_API_KEY!)
export async function sendVerificationEmail(options: SendVerificationEmailOptions): Promise<{ success: boolean; error?: string }> {
const { to, verifyUrl, userName } = options
try {
await sgMail.send({
to,
from: {
email: process.env.EMAIL_FROM_ADDRESS!,
name: process.env.EMAIL_FROM_NAME!,
},
subject: 'Verify your email address',
html: getVerificationEmailHtml({ verifyUrl, userName }),
text: getVerificationEmailText({ verifyUrl, userName }),
})
return { success: true }
} catch (err: any) {
console.error('SendGrid error:', err.response?.body || err)
return { success: false, error: 'Failed to send email' }
}
}
Option C: Nodemailer (SMTP)
npm install nodemailer
npm install -D @types/nodemailer
// src/lib/email/nodemailer.ts
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
export async function sendVerificationEmail(options: SendVerificationEmailOptions): Promise<{ success: boolean; error?: string }> {
const { to, verifyUrl, userName } = options
try {
await transporter.sendMail({
from: `"${process.env.EMAIL_FROM_NAME}" <${process.env.EMAIL_FROM_ADDRESS}>`,
to,
subject: 'Verify your email address',
html: getVerificationEmailHtml({ verifyUrl, userName }),
text: getVerificationEmailText({ verifyUrl, userName }),
})
return { success: true }
} catch (err) {
console.error('SMTP error:', err)
return { success: false, error: 'Failed to send email' }
}
}
Email Templates
HTML Template
// src/lib/email/templates/verification.ts
interface TemplateOptions {
verifyUrl: string
userName?: string
}
export function getVerificationEmailHtml({ verifyUrl, userName }: TemplateOptions): string {
const greeting = userName ? `Hi ${userName},` : 'Hi,'
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: #f9fafb; border-radius: 8px; padding: 32px; margin: 20px 0;">
<h1 style="color: #111; font-size: 24px; margin: 0 0 16px;">Verify your email</h1>
<p style="margin: 0 0 24px;">${greeting}</p>
<p style="margin: 0 0 24px;">
Thanks for signing up! Please verify your email address by clicking the button below:
</p>
<a href="${verifyUrl}"
style="display: inline-block; background: #0070f3; color: white; padding: 12px 24px;
border-radius: 6px; text-decoration: none; font-weight: 500;">
Verify Email
</a>
<p style="margin: 24px 0 0; font-size: 14px; color: #666;">
This link expires in 24 hours. If you didn't create an account, you can safely ignore this email.
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 24px 0;">
<p style="margin: 0; font-size: 12px; color: #999;">
If the button doesn't work, copy and paste this link:<br>
<a href="${verifyUrl}" style="color: #0070f3; word-break: break-all;">${verifyUrl}</a>
</p>
</div>
</body>
</html>
`
}
export function getVerificationEmailText({ verifyUrl, userName }: TemplateOptions): string {
const greeting = userName ? `Hi ${userName},` : 'Hi,'
return `
${greeting}
Thanks for signing up! Please verify your email address by clicking the link below:
${verifyUrl}
This link expires in 24 hours. If you didn't create an account, you can safely ignore this email.
`.trim()
}
Environment Variables
# Email Provider (choose one)
RESEND_API_KEY=re_xxxxx
# or
SENDGRID_API_KEY=SG.xxxxx
# or
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=user
SMTP_PASS=pass
# Common
EMAIL_FROM_NAME=Your App
EMAIL_FROM_ADDRESS=noreply@yourdomain.com
Unified Email Service
// src/lib/email/index.ts
import { VERIFICATION_CONFIG } from '@/lib/verification/config'
// Import your chosen provider
import { sendVerificationEmail as sendEmail } from './resend'
// import { sendVerificationEmail as sendEmail } from './sendgrid'
export async function sendVerificationEmail(
email: string,
token: string,
userName?: string
): Promise<{ success: boolean; error?: string }> {
const verifyUrl = VERIFICATION_CONFIG.getVerifyUrl(token)
return sendEmail({
to: email,
verifyUrl,
userName,
})
}
Success Criteria
- β’ Email provider configured and authenticated
- β’ Test email sends successfully
- β’ Verification link is clickable and correct
- β’ Plain text fallback renders properly
- β’ Error handling returns meaningful messages
Related Files
- β’Create:
src/lib/email/[provider].ts - β’Create:
src/lib/email/templates/verification.ts - β’Create:
src/lib/email/index.ts - β’Modify:
.env.example