How to Send Emails in Nuxt (Nuxt 3, 2026 Guide)

Nuxt 3 runs on Nitro, which gives you server routes that work like any Node.js backend. You can send emails directly from your Nuxt app without a separate server. API keys stay in runtimeConfig on the server side, never exposed to the client.
This guide covers server routes, composables, and the patterns you need for production email sending in Nuxt.
Pick a Provider
- Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences from one SDK. Native Stripe integration.
- Resend is developer-friendly. They have one-off broadcast campaigns but no automations or sequences.
- SendGrid is the enterprise option. Good for high volume.
Install
npm install sequenzynpm install resendnpm install @sendgrid/mailConfigure in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-only keys (never exposed to client)
sequenzyApiKey: process.env.SEQUENZY_API_KEY,
resendApiKey: process.env.RESEND_API_KEY,
sendgridApiKey: process.env.SENDGRID_API_KEY,
},
});Create a Server Utility
import Sequenzy from "sequenzy";
let client: Sequenzy | null = null;
export function useEmail() {
if (!client) {
const config = useRuntimeConfig();
client = new Sequenzy({ apiKey: config.sequenzyApiKey });
}
return client;
}import { Resend } from "resend";
let client: Resend | null = null;
export function useEmail() {
if (!client) {
const config = useRuntimeConfig();
client = new Resend(config.resendApiKey);
}
return client;
}import sgMail from "@sendgrid/mail";
let initialized = false;
export function useEmail() {
if (!initialized) {
const config = useRuntimeConfig();
sgMail.setApiKey(config.sendgridApiKey);
initialized = true;
}
return sgMail;
}Send from a Server Route
export default defineEventHandler(async (event) => {
const { email, name } = await readBody(event);
if (!email || !name) {
throw createError({ statusCode: 400, message: "email and name required" });
}
const sequenzy = useEmail();
const result = await sequenzy.transactional.send({
to: email,
subject: `Welcome, ${name}`,
body: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
});
return { jobId: result.jobId };
});export default defineEventHandler(async (event) => {
const { email, name } = await readBody(event);
if (!email || !name) {
throw createError({ statusCode: 400, message: "email and name required" });
}
const resend = useEmail();
const { data, error } = await resend.emails.send({
from: "Your App <noreply@yourdomain.com>",
to: email,
subject: `Welcome, ${name}`,
html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
});
if (error) {
throw createError({ statusCode: 500, message: error.message });
}
return { id: data?.id };
});export default defineEventHandler(async (event) => {
const { email, name } = await readBody(event);
if (!email || !name) {
throw createError({ statusCode: 400, message: "email and name required" });
}
const sgMail = useEmail();
try {
await sgMail.send({
to: email,
from: "noreply@yourdomain.com",
subject: `Welcome, ${name}`,
html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
});
return { sent: true };
} catch {
throw createError({ statusCode: 500, message: "Failed to send" });
}
});Call from a Vue component:
<!-- pages/contact.vue -->
<script setup lang="ts">
const email = ref('');
const name = ref('');
const status = ref<'idle' | 'sending' | 'sent' | 'error'>('idle');
async function handleSubmit() {
status.value = 'sending';
try {
await $fetch('/api/send-welcome', {
method: 'POST',
body: { email: email.value, name: name.value },
});
status.value = 'sent';
} catch {
status.value = 'error';
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="name" placeholder="Name" required />
<button type="submit" :disabled="status === 'sending'">
{{ status === 'sending' ? 'Sending...' : 'Send' }}
</button>
<p v-if="status === 'sent'" style="color: green">Email sent!</p>
<p v-if="status === 'error'" style="color: red">Failed to send</p>
</form>
</template>Going to Production
1. Verify Your Domain
Add SPF, DKIM, DMARC DNS records through your provider's dashboard.
2. Keep Keys Server-Side
Only use runtimeConfig (not runtimeConfig.public) for API keys. Nuxt guarantees server-only keys never reach the client.
3. Use $fetch for API Calls
Nuxt's $fetch handles SSR and client-side transparently. During SSR it calls the server route directly (no HTTP overhead).
Beyond Transactional
Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one SDK. Native Stripe integration for SaaS.
Wrapping Up
- Nitro server routes for API-style email endpoints
- Server utilities for reusable email clients
- runtimeConfig for secure API key management
$fetchfor seamless client-server communication
Pick your provider, copy the patterns, and start sending.