This boilerplate uses Resend by default for sending transactional emails, but is designed with an abstraction layer that makes it easy to swap providers.
Overview
The email system provides:
- Transactional emails - Auth emails, notifications, support messages
- Beautiful templates - HTML email templates included
- Provider abstraction - Easy to switch email providers
- Type-safe - Full TypeScript support
- Server-side only - Emails sent from secure server endpoints
Configuration
Environment variables
Add your Resend API key to .env:
RESEND_API_KEY="re_..."
SUPPORT_FORM_TARGET_EMAIL="support@yourdomain.com"
Email configuration
Email settings are in server/utils/config.ts:
export const EMAIL_CONFIG = {
FROM_EMAIL: `${config.public.siteName} <no-reply@${config.public.siteDomain}>`,
} as const
Sending emails
The email service is located in server/services/email-server-service.ts:
export async function sendEmail({ to, subject, html }: { to: string; subject: string; html: string }) {
return resend.emails.send({
from: EMAIL_CONFIG.FROM_EMAIL,
to,
subject,
html,
})
}
Example usage
In your API route:
import { sendEmail } from '~/server/services/email-server-service'
export default defineEventHandler(async event => {
const { email, name } = await readBody(event)
await sendEmail({
to: email,
subject: 'Welcome to our app!',
html: `
<h1>Welcome ${name}!</h1>
<p>Thanks for signing up.</p>
`,
})
return { success: true }
})
Email templates
Pre-built templates are in server/email-templates/:
Verify email template
export const verifyEmailTemplate = `
<!DOCTYPE html>
<html>
<head>
<style>
/* Email styles */
</style>
</head>
<body>
<h1>Verify your email</h1>
<p>Click the button below to verify your email address:</p>
<a href="{{action_url}}" style="...">Verify Email</a>
</body>
</html>
`
Template placeholders
Templates use double curly braces for dynamic content:
{{action_url}}- Action link (verify email, reset password){{site_name}}- Your site name from env{{site_domain}}- Your site domain from env{{logo_url}}- Your logo URL{{otp}}- One-time password code{{newEmail}}- New email address (for email changes)
Using templates
Replace placeholders before sending:
const html = verifyEmailTemplate
.replaceAll('{{action_url}}', verificationUrl)
.replaceAll('{{site_name}}', config.public.siteName)
.replaceAll('{{logo_url}}', logoUrl)
await sendEmail({
to: user.email,
subject: `Verify your email for ${config.public.siteName}`,
html,
})
Creating custom templates
To create a new email template:
- Create the template file:
export const welcomeEmailTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
}
</style>
</head>
<body>
<img src="{{logo_url}}" alt="Logo" style="max-width: 120px;">
<h1>Welcome to {{site_name}}, {{user_name}}!</h1>
<p>We're excited to have you on board.</p>
<a href="{{dashboard_url}}" class="button">Get Started</a>
<p style="color: #666; font-size: 12px;">
© {{site_name}}. All rights reserved.
</p>
</body>
</html>
`
- Create a helper function:
import { welcomeEmailTemplate } from '../email-templates/welcomeEmailTemplate'
export function prepareWelcomeEmail(user: { name: string; email: string }) {
const config = useRuntimeConfig()
const html = welcomeEmailTemplate
.replaceAll('{{user_name}}', user.name)
.replaceAll('{{site_name}}', config.public.siteName)
.replaceAll('{{logo_url}}', `${config.public.siteUrl}/logo-180px.png`)
.replaceAll('{{dashboard_url}}', `${config.public.siteUrl}/dashboard`)
return {
to: user.email,
subject: `Welcome to ${config.public.siteName}!`,
html,
}
}
- Use it in your code:
import { sendEmail } from '~/server/services/email-server-service'
import { prepareWelcomeEmail } from '~/server/utils/email-helpers'
const emailData = prepareWelcomeEmail(user)
await sendEmail(emailData)
Support form emails
Send support form submissions to your email:
import { sendSupportEmail } from '~/server/services/email-server-service'
export default defineEventHandler(async event => {
const { name, email, message } = await readBody(event)
await sendSupportEmail({
subject: `Support request from ${name}`,
html: `
<h2>New support request</h2>
<p><strong>From:</strong> ${name} (${email})</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
})
return { success: true }
})
Replacing Resend with another provider
To switch to a different email provider:
Option 1: Simple replacement
Replace the Resend client in server/services/email-server-service.ts:
// Before: Resend
import { Resend } from 'resend'
const resend = new Resend(config.resendApiKey)
// After: SendGrid example
import sgMail from '@sendgrid/mail'
sgMail.setApiKey(config.sendgridApiKey)
export async function sendEmail({ to, subject, html }) {
return sgMail.send({
from: EMAIL_CONFIG.FROM_EMAIL,
to,
subject,
html,
})
}
Option 2: Create an adapter
For more flexibility, create an email adapter pattern:
- Create the adapter interface:
export interface EmailAdapter {
send(params: { from: string; to: string; subject: string; html: string }): Promise<any>
}
- Implement provider adapters:
import { Resend } from 'resend'
import type { EmailAdapter } from '~/server/utils/email-adapter'
export class ResendAdapter implements EmailAdapter {
private client: Resend
constructor(apiKey: string) {
this.client = new Resend(apiKey)
}
async send({ from, to, subject, html }) {
return this.client.emails.send({ from, to, subject, html })
}
}
import sgMail from '@sendgrid/mail'
import type { EmailAdapter } from '~/server/utils/email-adapter'
export class SendGridAdapter implements EmailAdapter {
constructor(apiKey: string) {
sgMail.setApiKey(apiKey)
}
async send({ from, to, subject, html }) {
return sgMail.send({ from, to, subject, html })
}
}
- Update the email service:
import { ResendAdapter } from '~/server/adapters/resend-adapter'
// import { SendGridAdapter } from '~/server/adapters/sendgrid-adapter'
const config = useRuntimeConfig()
const emailAdapter = new ResendAdapter(config.resendApiKey)
// const emailAdapter = new SendGridAdapter(config.sendgridApiKey)
export async function sendEmail({ to, subject, html }) {
return emailAdapter.send({
from: EMAIL_CONFIG.FROM_EMAIL,
to,
subject,
html,
})
}
Popular email providers
Here are some popular alternatives to Resend:
- SendGrid - sendgrid.com
- Mailgun - mailgun.com
- Postmark - postmarkapp.com
- AWS SES - aws.amazon.com/ses
- Mailchimp Transactional - mailchimp.com/developer/transactional/
Best practices
Always test emails in development
Use a service like Mailtrap to test emails without sending to real addresses:
const isDev = process.env.NODE_ENV === 'development'
const transport = isDev
? createMailtrapTransport() // Catches emails in dev
: createProductionTransport() // Sends real emails in prod
Use plain text alternatives
Some email clients don't support HTML. Include plain text:
await sendEmail({
to: user.email,
subject: 'Welcome!',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
text: 'Welcome! Thanks for signing up.', // Plain text version
})
Handle email failures gracefully
try {
await sendEmail({
/* ... */
})
} catch (error) {
logger.error('Failed to send email:', error)
// Don't throw - log the error but don't block the user
// Consider adding to a retry queue
}
Respect email limits
Most providers have rate limits. For bulk emails:
- Use a proper email marketing service (Mailchimp, SendGrid Marketing)
- Implement queuing for large batches
- Add delays between sends if needed
Testing emails
Manual testing
- Set up a test email in your
.env - Trigger the email flow (sign up, password reset, etc.)
- Check your inbox
Automated testing
Mock the email service in your tests:
// vitest example
import { vi } from 'vitest'
import * as emailService from '~/server/services/email-server-service'
vi.spyOn(emailService, 'sendEmail').mockResolvedValue({ success: true })
// Test your code that sends emails
Troubleshooting
Emails not sending
- Check your API key - Verify it's correct in
.env - Check sender domain - Some providers require domain verification
- Check logs - Look for error messages in your server logs
- Check spam folder - Emails might be filtered
Emails look broken
- Test HTML - Use an email testing tool like Litmus
- Inline CSS - Email clients have poor CSS support, inline styles work best
- Use tables - For layout, tables are more reliable than flexbox/grid