Email Verification System/Phase 3 of 530-45 minutes

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

ProviderProsConsFree Tier
ResendSimple API, React EmailNewer3,000/month
SendGridMature, good analyticsComplex setup100/day
PostmarkFast delivery, great DXNo marketing email100/month
AWS SESCheap at scaleRequires AWS setup62,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