This is a demo site.Purchase now

Email

Send transactional emails with Resend or your preferred provider

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:

.env
RESEND_API_KEY="re_..."
SUPPORT_FORM_TARGET_EMAIL="support@yourdomain.com"
Get your Resend API key from the Resend dashboard.

Email configuration

Email settings are in server/utils/config.ts:

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:

server/utils/config.ts
export const EMAIL_CONFIG = {
  FROM_EMAIL: 'hello@mydomain.com',
} as const

Sending emails

The email service is located in server/services/email-server-service.ts:

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:

server/api/send-notification.post.ts
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

server/email-templates/verifyEmailTemplate.ts
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:

  1. Create the template file:
server/email-templates/welcomeEmailTemplate.ts
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>
`
  1. 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: '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 }
})
This matches the pattern used throughout the codebase (see server/utils/auth.ts for examples). For frequently sent emails, you can extract this into a reusable function.

Support form emails

The template includes a working support form that sends emails. The pattern is:

  1. Frontend builds the email (app/components/support/SupportForm.vue)
  2. Client service calls API (app/services/email-client-service.ts)
  3. Server endpoint sends email (server/api/email/send-support.ts)

The server endpoint includes rate limiting (5 requests per hour per IP):

server/api/email/send-support.ts
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 })
})
Check app/components/support/SupportForm.vue to see how the frontend builds the email HTML with user and app details.

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:

server/services/email-server-service.ts
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,
  })
}
Remember to update both sendEmail and sendSupportEmail functions, and add your new provider's API key to .env.

Here are some popular alternatives to Resend:

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

Reference

For production use, make sure to set up SPF, DKIM, and DMARC records for your domain to improve email deliverability.