Back to Blog

How to Send Emails in Astro (2026 Guide)

11 min read

Astro is a static-first framework, but it supports server-side rendering (SSR) and API endpoints. With SSR enabled, you can send emails from server endpoints just like any Node.js backend. API keys stay on the server.

This guide covers Astro server endpoints, form handling, and production patterns for email sending.

Enable SSR

You need SSR mode to run server-side code. Add an adapter:

npx astro add node  # or vercel, netlify, cloudflare
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
 
export default defineConfig({
  output: 'server',  // or 'hybrid' for mixed static + server
  adapter: node({ mode: 'standalone' }),
});

Install

Terminal
npm install sequenzy
Terminal
npm install resend
Terminal
npm install @sendgrid/mail

Add your API key to .env:

.env
SEQUENZY_API_KEY=sq_your_api_key_here
.env
RESEND_API_KEY=re_your_api_key_here
.env
SENDGRID_API_KEY=SG.your_api_key_here

Create a shared email client:

src/lib/email.ts
import Sequenzy from "sequenzy";

export const sequenzy = new Sequenzy();
// Reads SEQUENZY_API_KEY from env automatically
src/lib/email.ts
import { Resend } from "resend";

export const resend = new Resend(import.meta.env.RESEND_API_KEY);
src/lib/email.ts
import sgMail from "@sendgrid/mail";

sgMail.setApiKey(import.meta.env.SENDGRID_API_KEY);

export { sgMail };

Server Endpoint

src/pages/api/send-welcome.ts
import type { APIRoute } from "astro";
import { sequenzy } from "../../lib/email";

export const POST: APIRoute = async ({ request }) => {
const { email, name } = await request.json();

if (!email || !name) {
  return new Response(JSON.stringify({ error: "email and name required" }), {
    status: 400,
  });
}

try {
  const result = await sequenzy.transactional.send({
    to: email,
    subject: `Welcome, ${name}`,
    body: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
  });

  return new Response(JSON.stringify({ jobId: result.jobId }));
} catch {
  return new Response(JSON.stringify({ error: "Failed to send" }), {
    status: 500,
  });
}
};
src/pages/api/send-welcome.ts
import type { APIRoute } from "astro";
import { resend } from "../../lib/email";

export const POST: APIRoute = async ({ request }) => {
const { email, name } = await request.json();

if (!email || !name) {
  return new Response(JSON.stringify({ error: "email and name required" }), {
    status: 400,
  });
}

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) {
  return new Response(JSON.stringify({ error: error.message }), {
    status: 500,
  });
}

return new Response(JSON.stringify({ id: data?.id }));
};
src/pages/api/send-welcome.ts
import type { APIRoute } from "astro";
import { sgMail } from "../../lib/email";

export const POST: APIRoute = async ({ request }) => {
const { email, name } = await request.json();

if (!email || !name) {
  return new Response(JSON.stringify({ error: "email and name required" }), {
    status: 400,
  });
}

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 new Response(JSON.stringify({ sent: true }));
} catch {
  return new Response(JSON.stringify({ error: "Failed to send" }), {
    status: 500,
  });
}
};

Form Handling in Astro Pages

Handle form submissions directly in an Astro page:

---
// src/pages/contact.astro
import { sequenzy } from "../lib/email";
 
let success = false;
let error = "";
 
if (Astro.request.method === "POST") {
  const data = await Astro.request.formData();
  const email = data.get("email") as string;
  const message = data.get("message") as string;
 
  if (!email || !message) {
    error = "All fields are required";
  } else {
    try {
      await sequenzy.transactional.send({
        to: "you@yourcompany.com",
        subject: `Contact from ${email}`,
        body: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
      });
      success = true;
    } catch {
      error = "Failed to send message";
    }
  }
}
---
 
<html>
  <body>
    <h1>Contact Us</h1>
    <form method="POST">
      <input name="email" type="email" placeholder="Your email" required />
      <textarea name="message" placeholder="Message" required></textarea>
      <button type="submit">Send</button>
    </form>
    {success && <p style="color: green">Message sent!</p>}
    {error && <p style="color: red">{error}</p>}
  </body>
</html>

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC DNS records.

2. Use import.meta.env for Server Variables

In Astro, env vars without the PUBLIC_ prefix are server-only.

3. Choose Your Output Mode

  • output: 'server' for full SSR
  • output: 'hybrid' for mostly static with some server routes

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one SDK. Native Stripe integration for SaaS.

Wrapping Up

  1. Server endpoints (src/pages/api/*.ts) for API-style email sending
  2. Astro page forms for server-rendered form handling
  3. SSR adapters for deployment to Node, Vercel, Netlify, etc.

Pick your provider, enable SSR, and start sending.