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.
- 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_..."
CONTACT_FORM_TO_EMAILS="[email protected]"
NUXT_PUBLIC_DISPLAY_CONTACT_EMAIL="[email protected]"
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
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: '[email protected]',
} 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>
`
- Use it in your code:
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: '[email protected]' }
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.Contact form emails
The template includes a contact form that stores inquiries and sends notification emails. The flow is:
- Frontend sends inquiry payload (
app/components/contact/ContactForm.vue) - Client service calls API (
app/services/contact-requests-client-service.ts) - Server endpoint validates + rate-limits (
server/api/contact-request/create.post.ts) - Server service stores inquiry record (
server/services/contact-requests-server-service.ts) - Email service sends notification (
server/services/email-server-service.ts)
The contact request endpoint includes rate limiting (5 requests per hour per IP):
import { createContactRequestRecord } from '@@/server/services/contact-requests-server-service'
import { sendContactEmail } from '@@/server/services/email-server-service'
import { rateLimit } from '@@/server/utils/rate-limit'
export default defineEventHandler(async event => {
await rateLimit(event, {
max: 5,
window: '1h',
prefix: 'contact-request',
})
const payload = createContactRequestSchema.parse(await readBody(event))
const user = event.context.user
const contactRequest = await createContactRequestRecord({
inquiryType: payload.inquiryType,
name: payload.name,
emailAddress: payload.emailAddress,
subject: payload.subject,
description: payload.description,
userId: typeof user?.id === 'string' ? user.id : null,
})
await sendContactEmail({
subject: payload.subject,
html: emailHtml,
})
return { contactRequest }
})
Inquiry table even when email delivery is degraded.Replacing Resend with another provider
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 sendContactEmail({ subject, html }: { subject: string; html: string }) {
const targetEmails = (config.contactFormToEmails || '')
.split(/[,;\n]/)
.map((email: string) => email.trim())
.filter(Boolean)
if (targetEmails.length === 0) {
throw createError({
statusCode: 500,
statusMessage: 'Contact email configuration is missing',
})
}
return sgMail.send({
from: EMAIL_CONFIG.FROM_EMAIL,
to: targetEmails,
subject,
html,
})
}
sendEmail and sendContactEmail functions, and add your new provider's API key to .env.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
Handle failures gracefully
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
}
Common issues
Emails not sending:
- Check your API key in
.env - Verify your sender domain (some providers require verification)
- Check your server logs for errors
- Look in spam folder
Emails look broken:
- Use inline CSS (email clients have limited CSS support)
- Use tables for layout instead of flexbox/grid
- Test with tools like Litmus
Rate limits: For bulk emails, use a proper email marketing service (Mailchimp, SendGrid Marketing) instead of transactional email APIs