Envello
Tutorial

Sending transactional email from Nuxt.js with Envello

Envello Team·2026-07-29·7 min read

Nuxt 3's server engine (Nitro) gives you server routes that run in Node.js, Deno, Cloudflare Workers, or any other supported runtime. Sending transactional email from a Nuxt application is a server-side operation: a single HTTP call from a server route or API endpoint, never from the browser.

Prerequisites

A Nuxt 3 project, an Envello account with a verified domain, and your API key in runtime config (not hardcoded, not in public config).

  • Add to nuxt.config.ts: runtimeConfig: { envelloApiKey: '', envelloFrom: '' }
  • Set NUXT_ENVELLO_API_KEY and NUXT_ENVELLO_FROM in your .env file
  • Nuxt auto-maps NUXT_ prefixed env vars to runtimeConfig keys (camelCase conversion)

Create a server utility

Nuxt's server/utils/ directory auto-imports functions into all server routes. Create a reusable email utility here.

  • Create server/utils/email.ts
  • Export an async function sendEmail({ to, subject, html, text? })
  • Access config with useRuntimeConfig() inside the function
  • Use $fetch('https://api.envello.dev/emails', { method: 'POST', headers: { Authorization: `Bearer ${config.envelloApiKey}` }, body: { from: config.envelloFrom, to, subject, html, text } })
  • Nitro's $fetch automatically serializes the body as JSON and throws on non-2xx responses

Sending from a server route

Create a server API route that handles form submissions or internal triggers.

  • Create server/api/send-email.post.ts
  • Read and validate the request body with readBody(event)
  • Call sendEmail({ to, subject, html }) with the validated data
  • Return { success: true, messageId } on success
  • Wrap in try/catch: use createError({ statusCode: 400 }) for validation errors, 500 for send failures

Calling from a Vue component

From your Vue component, use useFetch or $fetch to call the server route. The email API key never reaches the browser; only the server route has access to runtimeConfig secrets.

  • In a composable or event handler: await $fetch('/api/send-email', { method: 'POST', body: { to, subject, html } })
  • Handle the response in the UI: show a success message or error state
  • Add client-side validation (email format, required fields) before calling the endpoint to avoid unnecessary server round-trips

Error handling

Nitro's $fetch throws an error on non-2xx responses, which makes error handling straightforward in server routes.

  • Catch FetchError in your server route's try/catch block
  • The error includes statusCode and data (the response body) from the API
  • For 400 errors: return the validation error to the client so they can fix their input
  • For 429 errors: the rate limit was hit; log and return a 'try again later' response
  • For 5xx errors: log the full error, return a generic 'email could not be sent' to the client
  • Never expose the raw API error body to the browser; it may contain internal details

Rendering HTML emails

For simple transactional emails (password reset, verification code), template literals in JavaScript work fine. For complex layouts, use a templating approach.

Vue's own template compiler isn't designed for email rendering (email HTML has different constraints than browser HTML). Instead, use MJML to author responsive email templates and compile them to HTML at build time, or write plain HTML with inline styles.

If you're sharing designs with a frontend team that uses Vue, consider creating email templates as standalone HTML files in a server/templates/ directory and loading them with a simple string replacement for variables. Keep email templates separate from Vue components.

Webhook handling

Receive delivery events from Envello through a server route.

  • Create server/api/webhooks/envello.post.ts
  • Read the raw body with readRawBody(event) for signature verification
  • Get the signature from getHeader(event, 'x-envello-signature')
  • Verify HMAC-SHA256 using Node.js crypto (or the Web Crypto API for edge deployments)
  • Process the event: delivered (log), bounced (mark address in your database), complained (suppress)
  • Return { received: true } with a 200 status

Deployment considerations

Nuxt's Nitro engine deploys to many targets. The email utility works across all of them because it only uses $fetch (which maps to the platform's native HTTP client).

  • Vercel: set NUXT_ENVELLO_API_KEY in project settings. Server routes run as serverless functions.
  • Cloudflare Workers/Pages: set the env var in wrangler.toml or the dashboard. $fetch uses the Workers fetch API automatically.
  • Node.js server (self-hosted): set the env var in your process environment or .env file.
  • All platforms: verify that runtimeConfig picks up the env var by logging config.envelloApiKey's presence (not its value) on first request during deployment testing.

Testing

Use Envello's test mode during development to avoid sending real emails. For unit tests, mock the $fetch call in your server utility and verify the request shape.

Nuxt's test utilities (nuxt-vitest) let you test server routes in isolation. Create a test that calls the route handler directly, mocking the email utility, and verify the request validation and response shape.

Free tool

Check your domain's SPF, DKIM, and DMARC records

Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.

Check your domain →
Envello

EU-hosted transactional email, done right by default.