← Back to all posts
2026-09-09 · MailAnvil Team

Send Transactional Emails from Vue.js + MailAnvil API — Server Routes Tutorial

TL;DR — This tutorial shows how to send transactional emails from a Vue.js application using MailAnvil's REST API. We'll use Nuxt 3 server routes (Nitro) as the backend, keeping API keys server-side while your Vue frontend stays clean.


Vue.js dominates the Indonesian frontend ecosystem. Whether you're building an admin dashboard, an e-commerce storefront, or a SaaS product — Vue is likely your framework of choice.

But when you need to send emails (order confirmations, password resets, OTP codes), you can't call an email API directly from the browser. Exposing your API key in client-side JavaScript is a security disaster.

The fix: Nuxt 3 server routes — Vue's built-in backend layer that runs on the server, keeps secrets safe, and calls your email API without CORS issues.

Here's exactly how to set it up with MailAnvil.

Prerequisites

Step 1: Create Your Nuxt 3 Project

npx nuxi@latest init mailanvil-vue-email
cd mailanvil-vue-email
npm install

Nuxt 3 uses Nitro under the hood — server routes live in the server/ directory and run on the Node.js server, never exposed to the browser.

Step 2: Add Your API Key to .env

Create a .env file in your project root:

MAILANVIL_API_KEY=re_your_api_key_here
[email protected]

Never commit .env to git. Nuxt 3 automatically loads .env variables in server routes via process.env.

Step 3: Create the Server Route

Create server/api/send-email.post.ts:

// server/api/send-email.post.ts
import { defineEventHandler, readBody } from 'h3'

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

  // Validate input
  if (!body.to || !body.subject || !body.html) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Missing required fields: to, subject, html'
    })
  }

  // Call MailAnvil API
  const response = await fetch('https://api.mailanvil.com/v1/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.MAILANVIL_API_KEY}`
    },
    body: JSON.stringify({
      from: process.env.MAILANVIL_FROM,
      to: body.to,
      subject: body.subject,
      html: body.html
    })
  })

  if (!response.ok) {
    const error = await response.json().catch(() => ({}))
    throw createError({
      statusCode: response.status,
      statusMessage: error.message || 'Email send failed'
    })
  }

  return { success: true, messageId: (await response.json()).id }
})

This route: - Runs server-side (API key never reaches the browser) - Validates input before calling MailAnvil - Returns a clean JSON response

Step 4: Call from Your Vue Component

Create pages/contact.vue:

<template>
  <div class="contact-form">
    <h1>Kirim Email</h1>
    <form @submit.prevent="sendEmail">
      <input v-model="form.to" type="email" placeholder="Recipient email" required />
      <input v-model="form.subject" type="text" placeholder="Subject" required />
      <textarea v-model="form.html" placeholder="Message" required />
      <button type="submit" :disabled="sending">
        {{ sending ? 'Sending...' : 'Send Email' }}
      </button>
    </form>
    <p v-if="result" :class="result.success ? 'success' : 'error'">
      {{ result.message }}
    </p>
  </div>
</template>

<script setup>
const form = reactive({ to: '', subject: '', html: '' })
const sending = ref(false)
const result = ref(null)

async function sendEmail() {
  sending.value = true
  result.value = null

  try {
    const res = await $fetch('/api/send-email', {
      method: 'POST',
      body: form
    })

    result.value = { success: true, message: 'Email sent!' }
    form.to = ''
    form.subject = ''
    form.html = ''
  } catch (err) {
    result.value = {
      success: false,
      message: err.data?.statusMessage || 'Failed to send email'
    }
  } finally {
    sending.value = false
  }
}
</script>

Notice: $fetch is Nuxt's built-in HTTP client that automatically handles the server route proxy. No CORS issues. No API key exposure.

Step 5: Run and Test

npm run dev

Visit http://localhost:3000, fill the form, and check your MailAnvil dashboard — the email should appear in your logs within seconds.

Production Considerations

Rate Limiting

MailAnvil has built-in rate limiting per API key. For your app, add server-side validation:

// server/api/send-email.post.ts — add before the MailAnvil call
const clientIp = getRequestHeader(event, 'x-forwarded-for') || 'unknown'
// Simple in-memory rate limiter (use Redis in production)
const key = `email:${clientIp}`
const count = await useStorage().getItem(key) || 0
if (count > 10) { // 10 emails per hour per IP
  throw createError({ statusCode: 429, statusMessage: 'Rate limit exceeded' })
}
await useStorage().setItem(key, count + 1, { maxAge: 3600 })

Error Handling

MailAnvil returns specific error codes: - 401 — Invalid API key - 403 — Domain not verified - 429 — Rate limit exceeded - 422 — Invalid email format

Map these to user-friendly messages in your frontend.

Templates

For recurring emails (welcome, invoice, OTP), use MailAnvil's template system instead of inline HTML:

body: JSON.stringify({
  from: process.env.MAILANVIL_FROM,
  to: body.to,
  template_id: 'welcome-email',
  variables: {
    name: body.name,
    activation_link: body.link
  }
})

Templates live in your MailAnvil dashboard — no code changes needed to update email copy.

MCP Integration

If you're building with AI agents (Claude Code, Cursor, Codex), MailAnvil is MCP-native. Add this to your .mcp.json:

{
  "mcpServers": {
    "mailanvil": {
      "url": "https://mcp.mailanvil.com/mcp",
      "headers": {
        "Authorization": "Bearer re_your_key"
      }
    }
  }
}

Your AI agent can then send emails directly during development — no manual API calls needed.

Pricing (IDR)

MailAnvil charges in Rupiah — no USD conversion surprises:

Plan Emails/mo Price
Free 500 Rp 0
Starter 10,000 Rp 149,000
Growth 50,000 Rp 499,000

Pay via QRIS, GoPay, or bank transfer. No credit card required.

Why Not SendGrid or Resend?

Both are great — but for Indonesian teams:

Next Steps

  1. Verify your domain for DKIM
  2. Set up webhooks for delivery tracking
  3. Browse the API reference for advanced features

Built for Indonesian developers. Powered by Cloudflare Workers + AWS SES.