This boilerplate uses Resend by default for sending transactional emails, but is designed with an abstraction layer that makes it easy to swap providers.
The email system provides:
Add your Resend API key to .env:
RESEND_API_KEY="re_..."
SUPPORT_FORM_TARGET_EMAIL="support@yourdomain.com"
Email settings are in server/utils/config.ts:
export const EMAIL_CONFIG = {
FROM_EMAIL: `${config.public.siteName} <no-reply@${config.public.siteDomain}>`,
} as const
Although the current configuration dynamically uses the site name and domain from environment variables, you can also simply set a static email address instead:
export const EMAIL_CONFIG = {
FROM_EMAIL: 'hello@mydomain.com',
} as const
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,
})
}
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 }
})
Pre-built templates are in server/email-templates/:
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>
`
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)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,
})
To create a new email template:
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>
`
import { sendEmail } from '@@/server/services/email-server-service'
import { welcomeEmailTemplate } from '@@/server/email-templates/welcomeEmailTemplate'
export default defineEventHandler(async event => {
const user = { name: 'John', email: 'john@example.com' }
const config = useRuntimeConfig()
// Replace template placeholders
const html = welcomeEmailTemplate
.replaceAll('{{user_name}}', user.name)
.replaceAll('{{site_name}}', config.public.siteName)
.replaceAll('{{logo_url}}', `${config.public.siteUrl}/logo-160px.png`)
.replaceAll('{{dashboard_url}}', `${config.public.siteUrl}/app/dashboard`)
// Send the email
await sendEmail({
to: user.email,
subject: `Welcome to ${config.public.siteName}!`,
html,
})
return { success: true }
})
server/utils/auth.ts for examples). For frequently sent emails, you can extract this into a reusable function.The template includes a working support form that sends emails. The pattern is:
app/components/support/SupportForm.vue)app/services/email-client-service.ts)server/api/email/send-support.ts)The server endpoint includes rate limiting (5 requests per hour per IP):
import { sendSupportEmail } from '@@/server/services/email-server-service'
import { rateLimit } from '@@/server/utils/rate-limit'
export default defineEventHandler(async event => {
// Rate limiting for email endpoints
await rateLimit(event, {
max: 5,
window: '1h',
prefix: 'support-email',
})
const { subject, html } = await readBody(event)
// Validate required fields
if (!subject?.trim() || !html?.trim()) {
throw createError({
statusCode: 400,
statusMessage: 'Subject and message are required',
})
}
return await sendSupportEmail({ subject, html })
})
app/components/support/SupportForm.vue to see how the frontend builds the email HTML with user and app details.While Resend works great out of the box, you may need to use a different provider based on your existing infrastructure or regional requirements. The email service is designed to make switching providers straightforward.
To switch providers, update server/services/email-server-service.ts with your provider's client. Here's an example using SendGrid:
import sgMail from '@sendgrid/mail'
import { createError } from 'h3'
import { EMAIL_CONFIG } from '../utils/config'
const config = useRuntimeConfig()
sgMail.setApiKey(config.sendgridApiKey)
export async function sendEmail({ to, subject, html }: { to: string; subject: string; html: string }) {
return sgMail.send({
from: EMAIL_CONFIG.FROM_EMAIL,
to,
subject,
html,
})
}
export async function sendSupportEmail({ subject, html }: { subject: string; html: string }) {
const targetEmail = config.supportFormTargetEmail
if (!targetEmail) {
throw createError({
statusCode: 500,
statusMessage: 'Support email configuration is missing',
})
}
return sgMail.send({
from: EMAIL_CONFIG.FROM_EMAIL,
to: targetEmail,
subject,
html,
})
}
sendEmail and sendSupportEmail functions, and add your new provider's API key to .env.Here are some popular alternatives to Resend:
Don't let email failures block your users:
try {
await sendEmail({ to: user.email, subject: 'Welcome!', html: '...' })
} catch (error) {
logger.error('Failed to send email:', error)
// Log the error but don't throw - user can still continue
}
Emails not sending:
.envEmails look broken:
Rate limits: For bulk emails, use a proper email marketing service (Mailchimp, SendGrid Marketing) instead of transactional email APIs