Skip to main content
By default, the Nuxt Lettermint module creates an endpoint at /api/lettermint/send for client-side email sending. However, you may want to create custom endpoints with additional logic, validation, or routing.

Why use custom endpoints?

Custom endpoints are useful when you need to:
  • Add custom validation before sending emails
  • Implement rate limiting or spam protection
  • Log email activity to your database
  • Transform or sanitize user input
  • Add authentication or authorization checks
  • Route different email types to different handlers
  • Integrate with other services before/after sending
If you only need to send emails from server code, you don’t need custom endpoints. Use sendEmail() directly in your server routes.

Disabling the auto-generated endpoint

To create custom endpoints, first disable the auto-generated endpoint in your Nuxt config:
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-lettermint'],
  lettermint: {
    autoEndpoint: false
  }
})
When you disable autoEndpoint, the client-side useLettermint() composable will not work unless you create a custom endpoint at /api/lettermint/send or modify your client code to use a different endpoint.

Creating a custom endpoint

Here’s the basic structure for a custom endpoint:
server/api/lettermint/send.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody } from 'h3'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  // Add your custom logic here
  
  return await sendEmail(body)
})
Create your custom endpoint at /api/lettermint/send.post.ts to maintain compatibility with the default useLettermint() composable.

Example: Custom validation

Add custom validation before sending emails:
server/api/lettermint/send.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody, createError } from 'h3'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Custom validation
  if (!body.from || !body.to || !body.subject) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Missing required fields: from, to, or subject'
    })
  }

  // Validate email domain
  if (!body.from.endsWith('@yourdomain.com')) {
    throw createError({
      statusCode: 403,
      statusMessage: 'From address must be from @yourdomain.com'
    })
  }

  // Ensure at least one content type
  if (!body.text && !body.html) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Either text or html content is required'
    })
  }

  return await sendEmail(body)
})

Example: Rate limiting

Implement rate limiting to prevent abuse:
server/api/lettermint/send.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody, createError, getRequestIP } from 'h3'

// Simple in-memory rate limiter (use Redis in production)
const rateLimits = new Map<string, { count: number, resetAt: number }>()

const RATE_LIMIT = 10 // emails per window
const RATE_WINDOW = 60 * 60 * 1000 // 1 hour in milliseconds

export default defineEventHandler(async (event) => {
  const ip = getRequestIP(event, { xForwardedFor: true })
  const now = Date.now()

  // Check rate limit
  const limit = rateLimits.get(ip)
  if (limit) {
    if (now < limit.resetAt) {
      if (limit.count >= RATE_LIMIT) {
        throw createError({
          statusCode: 429,
          statusMessage: 'Too many emails sent. Please try again later.'
        })
      }
      limit.count++
    } else {
      // Reset window
      rateLimits.set(ip, { count: 1, resetAt: now + RATE_WINDOW })
    }
  } else {
    rateLimits.set(ip, { count: 1, resetAt: now + RATE_WINDOW })
  }

  const body = await readBody(event)
  return await sendEmail(body)
})
The above example uses in-memory storage. For production, use Redis or a similar persistent store.

Example: Logging and analytics

Log email activity to your database:
server/api/lettermint/send.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody, getRequestIP } from 'h3'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const ip = getRequestIP(event, { xForwardedFor: true })

  try {
    // Send the email
    const result = await sendEmail(body)

    // Log successful send to database
    await db.emailLogs.create({
      messageId: result.message_id,
      from: body.from,
      to: Array.isArray(body.to) ? body.to.join(', ') : body.to,
      subject: body.subject,
      status: 'sent',
      sentAt: new Date(),
      ipAddress: ip,
      tags: body.tags || []
    })

    return {
      success: true,
      messageId: result.message_id,
      status: result.status
    }
  }
  catch (error) {
    // Log failed send
    await db.emailLogs.create({
      from: body.from,
      to: Array.isArray(body.to) ? body.to.join(', ') : body.to,
      subject: body.subject,
      status: 'failed',
      error: (error as Error).message,
      sentAt: new Date(),
      ipAddress: ip
    })

    throw error
  }
})

Example: Authentication check

Require authentication before sending emails:
server/api/lettermint/send.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody, createError, getHeader } from 'h3'

export default defineEventHandler(async (event) => {
  // Check for auth token
  const authToken = getHeader(event, 'authorization')
  if (!authToken) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Authentication required'
    })
  }

  // Verify token (example)
  const user = await verifyAuthToken(authToken)
  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid authentication token'
    })
  }

  const body = await readBody(event)

  // Add user info to metadata
  const result = await sendEmail({
    ...body,
    metadata: {
      ...body.metadata,
      userId: user.id,
      userEmail: user.email
    },
    tags: [...(body.tags || []), `user:${user.id}`]
  })

  return {
    success: true,
    messageId: result.message_id,
    status: result.status
  }
})

Example: Input sanitization

Sanitize user input to prevent XSS or injection attacks:
server/api/lettermint/send.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody } from 'h3'
import DOMPurify from 'isomorphic-dompurify'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Sanitize HTML content
  if (body.html) {
    body.html = DOMPurify.sanitize(body.html, {
      ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'a', 'strong', 'em', 'ul', 'ol', 'li'],
      ALLOWED_ATTR: ['href', 'target']
    })
  }

  // Sanitize text content (strip HTML)
  if (body.text) {
    body.text = body.text.replace(/<[^>]*>/g, '')
  }

  // Sanitize subject
  if (body.subject) {
    body.subject = body.subject.replace(/<[^>]*>/g, '').trim()
  }

  return await sendEmail(body)
})

Example: Multiple custom endpoints

Create separate endpoints for different email types:
// server/api/contact.post.ts
import { sendEmail } from '#imports'
import { defineEventHandler, readBody } from 'h3'

export default defineEventHandler(async (event) => {
  const { name, email, message } = await readBody(event)

  return await sendEmail({
    from: '[email protected]',
    to: '[email protected]',
    replyTo: email,
    subject: `Contact Form: ${name}`,
    html: `
      <h2>New Contact Form Submission</h2>
      <p><strong>From:</strong> ${name} (${email})</p>
      <p><strong>Message:</strong></p>
      <p>${message}</p>
    `,
    tags: ['contact-form']
  })
})

Using custom endpoints from the client

If you create custom endpoints at different paths, update your client code:
Contact form component
<script setup>
import { ref } from 'vue'
import { $fetch } from 'ofetch'

const sending = ref(false)
const error = ref(null)

const sendContactForm = async (formData) => {
  sending.value = true
  error.value = null

  try {
    await $fetch('/api/contact', {
      method: 'POST',
      body: formData
    })
    // Success!
  } catch (err) {
    error.value = err.message
  } finally {
    sending.value = false
  }
}
</script>
If you create a custom endpoint at /api/lettermint/send, the default useLettermint() composable will work without any changes.

Reference: Default endpoint implementation

Here’s the complete implementation of the auto-generated endpoint for reference:
src/runtime/server/api/lettermint/send.post.ts
import { defineEventHandler, readBody, createError } from 'h3'
import { sendEmail, type SendEmailOptions } from '../../utils/lettermint'

export default defineEventHandler(async (event) => {
  try {
    const body = await readBody<SendEmailOptions>(event)

    // Validate required fields
    if (!body.from) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Missing required field: from',
      })
    }

    if (!body.to) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Missing required field: to',
      })
    }

    if (!body.subject) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Missing required field: subject',
      })
    }

    if (!body.text && !body.html) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Either text or html content is required',
      })
    }

    // Send the email
    const result = await sendEmail(body)

    return {
      success: true,
      messageId: result.message_id,
      status: result.status,
    }
  }
  catch (error: unknown) {
    const err = error as any

    // Handle Lettermint SDK errors with responseBody
    if (err.responseBody?.message) {
      throw createError({
        statusCode: err.statusCode || 422,
        statusMessage: err.responseBody.message,
      })
    }

    // Handle validation errors
    if (err.statusCode) {
      throw createError({
        statusCode: err.statusCode,
        statusMessage: err.message || 'Validation error',
      })
    }

    // Handle other errors
    throw createError({
      statusCode: 500,
      statusMessage: err.message || 'Internal server error while sending email',
    })
  }
})

Best practices

Even though sendEmail() validates required fields, add your own validation for security and better error messages.
Protect your API from abuse with rate limiting, especially for public-facing endpoints.
Keep track of sent emails for debugging, analytics, and compliance purposes.
Always sanitize HTML content and user-provided data to prevent XSS attacks.
Require authentication for email endpoints to prevent unauthorized use.
Return helpful error messages while avoiding exposing sensitive information.

Next steps

Server-side usage

Learn about the sendEmail() function

Configuration

Configure the module for your needs

Build docs developers (and LLMs) love