Envello
Tutorial

Sending transactional email from SvelteKit with Envello

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

SvelteKit has a clean separation between server and client code. Server-only modules (files ending in .server.ts, +page.server.ts form actions, and +server.ts API routes) are the right place for transactional email sends. The API key stays on the server, and the email is sent as part of a form action or API call.

Prerequisites

A SvelteKit project, an Envello account with a verified domain, and your API key in a .env file. SvelteKit loads .env files automatically via Vite.

  • ENVELLO_API_KEY=your_api_key_here
  • ENVELLO_FROM=YourApp <[email protected]>
  • Access these with import { ENVELLO_API_KEY, ENVELLO_FROM } from '$env/static/private'
  • The $env/static/private module is server-only; importing it in a client file causes a build error, which is a safety feature

Create the email utility

Create a server-only module that wraps the Envello API call.

  • Create src/lib/server/email.ts (the server/ directory makes it importable only from server code)
  • Import { ENVELLO_API_KEY, ENVELLO_FROM } from '$env/static/private'
  • Export an async function sendEmail({ to, subject, html, text? }): Promise<{ messageId: string }>
  • Use fetch() to POST to https://api.envello.dev/emails with the Authorization header and JSON body
  • If !response.ok, throw an error with the status and response text
  • Return the parsed response body ({ messageId })

Sending from a form action

Form actions are SvelteKit's primary mechanism for handling form submissions with progressive enhancement. They're the natural place to trigger transactional email sends.

  • In +page.server.ts, define a named action: export const actions = { resetPassword: async ({ request }) => { ... } }
  • Parse the form data with await request.formData()
  • Validate the email address (format, required)
  • Generate the reset token (your app logic)
  • Call sendEmail({ to: email, subject: 'Reset your password', html: renderResetEmail(token) })
  • Return { success: true } or use fail(400, { error: 'message' }) for validation errors
  • In +page.svelte, use the form action with <form method="POST" action="?/resetPassword"> and use:enhance for progressive enhancement

Sending from an API route

For webhook handlers, cron triggers, or external API consumers, use a +server.ts route.

  • Create src/routes/api/send-email/+server.ts
  • Export an async POST handler: export async function POST({ request }) { ... }
  • Parse the JSON body, validate, and call sendEmail()
  • Return json({ success: true, messageId }) on success
  • Return json({ error }, { status: 400 }) on validation failure
  • Protect the endpoint: check for an API key header or session cookie before processing

Error handling

Handle errors at the utility level and surface them cleanly in form actions.

  • In sendEmail(), throw typed errors: new EmailSendError('validation', 'Invalid recipient') for 400s, new EmailSendError('rateLimit', 'Rate limited') for 429s
  • In the form action, catch the error and use fail() to return it to the page: return fail(500, { error: 'Could not send email. Try again.' })
  • In +page.svelte, access the error via the form prop: {#if form?.error}<p>{form.error}</p>{/if}
  • Never expose raw API errors to the user; map them to user-friendly messages

Rendering email HTML

SvelteKit's component rendering is for browsers, not email clients. For email HTML, use a separate approach.

For simple emails: template literals with HTML strings work fine. A password reset email is a heading, a paragraph, and a link.

For complex layouts: use MJML to design responsive emails and compile to HTML at build time, or use React Email (it works outside React projects as a standalone rendering tool via the CLI).

Store email templates in src/lib/server/templates/ as functions that accept parameters and return HTML strings. This keeps them testable and version-controlled.

Webhook endpoint

Receive Envello delivery events through a +server.ts route.

  • Create src/routes/api/webhooks/envello/+server.ts
  • Export a POST handler
  • Read the raw body with await request.text() for signature verification
  • Get the signature from request.headers.get('x-envello-signature')
  • Verify HMAC-SHA256 using Node's crypto module (available in SvelteKit's default Node adapter) or the Web Crypto API for edge deployments
  • Return new Response('ok', { status: 200 }) immediately after queuing the event for processing

Adapter considerations

SvelteKit deploys via adapters. The email utility works across all of them because it only uses fetch().

  • adapter-node: env vars from process.env or .env file. Full Node.js crypto available for webhook verification.
  • adapter-vercel: env vars from Vercel project settings. Server functions run as serverless functions.
  • adapter-cloudflare: env vars from wrangler.toml or the dashboard. Use $env/dynamic/private to read platform-specific env bindings instead of $env/static/private.
  • adapter-auto: works in development; verify env var availability in your deployment target.

Testing

Mock the sendEmail function in your form action tests using vi.mock('$lib/server/email'). Verify the action validates input correctly, calls sendEmail with the right arguments, and returns the expected success or error response.

For integration tests, use Envello's test mode: API calls are accepted and validated but no email is delivered. This tests the full path from form submission to API call.

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.