skip to content

Send Serverless Email with Cloudflare Email Routing in Astro

/ 7 min read

cloudflare email routing

We’ve our applicaton ready but we want to add email for notification, OTP, Greetiings. This is where Cloudflare Email comes into play. Ini this article we wiill implement how to send emails directly from your Astro site using Cloudflare Worker bindings — no external REST APIs, no API keys, and no traditional backend. We’ll leverage Cloudflare’s native Email Worker bindings combined with Email Routing to send emails from your domain.

By the end, your Astro site will have a contact form that sends emails using purely Cloudflare’s infrastructure — fast, secure, and serverless.

Why Cloudflare - Serverless Email ?

  • Worker Bindings over REST – Direct, low-latency communication between your Astro serverless function and Cloudflare’s email system. No HTTP overhead.
  • Email Routing as Sender Infrastructure – Once you set up Email Routing for your domain, any email sent via Workers is automatically authenticated and deliverable.
  • No third-party fees – MailChannels integration is free within Workers when you verify your domain.
  • Global by default – Your email-sending logic runs on 300+ Cloudflare locations.
  • Tight security – Bindings are internal to Cloudflare, no API keys to leak.

Astro Awesome Islands Architecture + Serverless Functions 💥

  • Islands architecture – Your contact form can be a static HTML island with zero client-side JavaScript. The email send happens via a <form> POST to a serverless endpoint.
  • Progressive enhancement – Even if JavaScript fails, the native form submission works because the endpoint is a standard HTTP POST.
  • Zero cold starts – Astro’s Cloudflare adapter compiles your endpoints to Workers that spin up in milliseconds.
  • Type-safe bindings – Access Cloudflare resources (like email bindings) directly via Astro.locals or the context object.

How it works

Static Astro page → User submits form → POST request to /api/send-email (serverless function) → Function uses Cloudflare Worker binding to send email → No external API calls.

Prerequisites

  • A Cloudflare account
  • A domain managed by Cloudflare (e.g., example.com)
  • Cloudflare Email Routing enabled for your domain (just the routing rule, no forwarding needed and its a PAID plan)
  • Node.js 18+ and an Astro project
Terminal window
pnpm create cloudflare@latest cloudflare-astro-email --framework=astro

We wll enter the above command for a new astro cloudflare compatiable setup. We are stiicking wiith a SSR setup which is a server rendered application so we miight also add a cloudflare adapter to our project.

Step 1: Enable Cloudflare Email Routing for your domain

Before sending emails, you must verify that your domain can send emails through Cloudflare.

  1. Go to Cloudflare Dashboard → your domain → EmailEmail Routing.
  2. Click Get started and complete the setup:
    • Add your domain (e.g., example.com)
    • Cloudflare will ask you to add MX records automatically (click “Add records”)
  3. Important: You don’t need to add destination addresses or forwarding rules. Just enable Email Routing for the domain.
  4. Once enabled, Cloudflare automatically handles SPF, DKIM, and DMARC for outgoing emails sent via Workers.

📌 Why no forwarding? We’re only sending emails from contact@example.com or noreply@example.com. The emails will go directly to recipients’ inboxes, not through a forwarding rule.

Step 2: Set up your Astro project with Cloudflare adapter

Terminal window
pnpm astro add cloudflare

In our project root, create a Wrangler configuration file with the following content:

Terminal window
{
"name": "my-astro-app",
"main": "./dist/_worker.js/index.js",
"compatibility_date": "2026-05-10",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"binding": "ASSETS",
"directory": "./dist"
},
"observability": {
"enabled": true
}
}

Update astro.config.mjs for server-side rendering (required for API endpoints):

import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import react from '@astrojs/react';
export default defineConfig({
output: 'server',
adapter: cloudflare({
//Allows to use wrangelr to acess KEY_BINDINGS into Astro endpoint
platformProxy: {
enabled: true
}
}),
integrations: [react()]
});

Step 3: Configure Worker bindings for email sending

We’ll use MailChannels via Cloudflare’s native binding. MailChannels is integrated into the Workers platform and requires no API key — just domain verification.

Add the following to your wrangler.jsonc file in the project root (create it if it doesn’t exist):

Terminal window
{
"compatibility_date": "2026-05-06",
"compatibility_flags": [
"global_fetch_strictly_public"
],
"name": "cloudflare-astro-email",
"main": "@astrojs/cloudflare/entrypoints/server",
"assets": {
"directory": "./dist",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
"$schema": "./node_modules/wrangler/config-schema.json",
"send_email": [
{
"name": "EMAIL"
}
]
}

Step 4: Create the email sending API endpoint with bindings

Inside src/pages/api/send-email.ts, create a POST endpoint that uses Cloudflare’s environment to send emails:

import type { APIRoute } from "astro";
import { env } from "cloudflare:workers";
const YOUR_DOMAIN = "hello@bikrima.app";
export const POST: APIRoute = async ({ request, params }) => {
const { to } = (await request.json()) as { to: string };
try {
const sendEmail = await env.EMAIL.send({
to: to,
from: `${YOUR_DOMAIN}`,
subject: "Welcome to For Those Who Code YT channel!",
html: "<h1>Hi!</h1><p>Thanks for checking us out.</p>",
text: "🙏Please subscribe, like for more video contents lke this.",
});
return new Response(
JSON.stringify({
message: "Email sent sucessfully!",
success: true,
messageId: sendEmail.messageId
}),
{
headers: {
"Content-Type": "application/json"
},
status: 200
}
);
} catch (error: any) {
return new Response(
JSON.stringify({
error: error.message,
}),
{
headers: {
"Content-Type": "application/json"
},
status: 400
}
);
}
};

Step 5: Create the email sending form component with Astro island

Create src/components/Email.tsx — a static form with progressive enhancement:

import { useState } from "react";
type Status = "idle" | "sending" | "success" | "error";
type DataResponse = {
message: string;
success: number;
messageId: string;
error: any;
};
export default function EmailForm() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [message, setMessage] = useState("");
const isSending = status === "sending";
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault();
if (!email.trim()) return;
setStatus("sending");
setMessage("");
try {
const res = await fetch("/api/send-email", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
to: email,
}),
});
const data: DataResponse = await res.json();
if (res.ok && data.success) {
setStatus("success");
setMessage(`Sent! Message ID: ${data.messageId}`);
setEmail("");
} else {
setStatus("error");
setMessage(data.error || "Something went wrong");
}
} catch (error) {
setStatus("error");
setMessage("Failed to reach the server");
}
};
return (
<div className="container">
<h1>⚡️ Send Email with Cloudflare Email!</h1>
<section className="card">
<p>
Enter an email address which sends email using the{" "}
<code>send_email</code> worker bindings.
</p>
<form className="send-form" onSubmit={handleSubmit}>
<input
type="email"
placeholder="recipient@example.com"
required
value={email}
disabled={isSending}
onChange={(e) => {
setEmail(e.target.value);
if (status !== "idle" && status !== "sending") {
setStatus("idle");
setMessage("");
}
}}
/>
<button type="submit" disabled={isSending}>
{isSending ? "Sending..." : "Send Email"}
</button>
</form>
{message && (
<p className={`status-message ${status}`}>
{message}
</p>
)}
</section>
</div>
);
}

Step 6: Use the contact form on a page

Lets use our contact form in our main page.

import EmailForm from '../components/Email';
import Layout from '../layouts/Layout.astro';
<Layout>
<EmailForm client:load />
</Layout>

Step 7: Deploy to Cloudflare

Below are command which wiill build and deploy our application.

Terminal window
pnpm build
pnpm deploy

Or deploy directly from the Cloudflare Dashboard by connecting your GitHub repository.

Step 8: Verify email sending

  • Visit https://yourdomain.com/contact
  • Fill out the form with your own email address
  • Submit the form
  • Check your inbox (check spam folder for the first few sends)

You should receive an email from contact@yourdomain.com containing the message details.

We now have a fully serverless email sending system using Cloudflare’s native infrastructure and Astro’s islands architecture. No external APIs, no forwarding rules — just pure email sending from your domain.

With this process we have configured and allowed our application to build with Cloudflare. We can also use Astro endpoints as it has configured the cloudflare adapter and server rendered is enabled. It will automatically be converted into a Cloudflare worker serverless function code 🤯.

Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I’ll do my best to get back to you.

You can find the complete source code in Github Link here.

Thank you!