April 14, 2026
·3 min read
Sending Mailgun Email Natively in Astro
When migrating a site from WordPress to Astro, one of the biggest challenges is replacing the things WordPress used to handle automatically.
In WordPress, if you want a contact form to send an email, you rely on wp_mail() and call it a day. In a custom framework like Astro, you build your own server connections.
When developers hit this point, their first instinct is usually to install a dedicated SDK package from Mailgun.
You probably don’t need to do that.
Installing an SDK just to power a single contact form bloats your server and gives you another dependency to maintain. Instead, use native web features.
The Native Fetch Setup
Mailgun offers a very straightforward API. Because modern server environments already have fetch() built in, we don’t need to download Mailgun’s code at all. We can hit their API directly.
Here is the logic I use inside src/pages/api/contact.ts to send emails without adding any third-party packages:
export const prerender = false; // Forces this route to run securely on the server
export async function POST({ request }: any) {
try {
// 1. Read the form data the user submitted
const formData = await request.formData();
const firstName = formData.get('first_name');
const email = formData.get('email');
const message = formData.get('message');
// 2. Set up our Mailgun details
const mailgunDomain = import.meta.env.MAILGUN_DOMAIN;
const mailgunEndpoint = `https://api.mailgun.net/v3/${mailgunDomain}/messages`;
// 3. Format the email naturally
const mailgunData = new URLSearchParams({
from: `Website Contact Form <postmaster@${mailgunDomain}>`,
to: "[email protected]",
subject: `New Message from ${firstName}`,
text: `Name: ${firstName}\nEmail: ${email}\n\nMessage:\n${message}`,
'h:Reply-To': email as string
});
const apiAuthKey = import.meta.env.MAILGUN_API_KEY;
// 4. Send the email directly to Mailgun
const emailRes = await fetch(mailgunEndpoint, {
method: 'POST',
headers: {
Authorization: 'Basic ' + btoa(`api:${apiAuthKey}`)
},
body: mailgunData
});
if (emailRes.ok) {
return new Response(JSON.stringify({ success: true }), { status: 200 });
} else {
return new Response(JSON.stringify({ error: "Mailgun API Failed." }), { status: 500 });
}
} catch (e: any) {
return new Response(JSON.stringify({ error: "Internal Server Error." }), { status: 500 });
}
}
Keep It Simple
There are a couple of engineering tricks happening here - like using btoa() to encode our passwords without external tools, and leveraging URLSearchParams to format data perfectly - but the core takeaway is simple.
We need to un-train our instinct to immediately npm install a new package for every problem. Every time you solve a problem using native web tools, you build a lighter, faster, and more resilient application.