Introduction
One of the most basic features of a website (and one of the most underestimated) is the contact form.
Between the limitations of mailto: and the complexity of a full backend, finding the right balance isn’t always obvious.
In this guide I’ll show you how I integrated Resend into my Astro project to handle email sending simply, reliably and securely.
1. Why Resend?
Resend is a modern email-sending service built for developers:
- no SMTP configuration to manage,
- a clear, fast API,
- and a clean dashboard to track what you send.
It’s also GDPR-friendly, unlike some of the more legacy solutions. For a portfolio, a landing page or a small client site, it’s a perfect fit: light, clean, effective.
2. Preparing the project
Install the official SDK:
npm install resend
Then add your API key to a .env file at the root of the project:
RESEND_API_KEY=your_resend_api_key_here
⚠️ Never expose this key on the client: it is handled server-side only, inside an Astro endpoint.
3. Creating an API route for sending
In Astro, endpoints let you add a bit of server logic without depending on a full framework.
Create a file:
src/pages/api/contact.ts
import type { APIRoute } from "astro";
import { Resend } from "resend";
const resend = new Resend(import.meta.env.RESEND_API_KEY);
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
try {
const { name, email, message } = data;
await resend.emails.send({
from: "Portfolio Contact <contact@yourdomain.dev>",
to: "hey@alxgb.dev",
subject: `New message from ${name}`,
reply_to: email,
text: message,
});
return new Response(
JSON.stringify({ success: true, message: "Email sent successfully" }),
{ status: 200 }
);
} catch (error) {
console.error(error);
return new Response(
JSON.stringify({ success: false, message: "Sending failed" }),
{ status: 500 }
);
}
};
What matters here: • The code runs server-side, so the API key stays private. • The endpoint returns a simple JSON response the frontend can act on.
4. Building the form on the client
On the front end, keep it simple:
a small form with a fetch to /api/contact, and a few display states depending on the response.
<form
onSubmit={async (e) => {
e.preventDefault();
const form = e.target;
const data = Object.fromEntries(new FormData(form));
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const { success } = await res.json();
form.reset();
alert(success ? "Message sent ✔" : "Something went wrong ❌");
}}
class="max-w-md space-y-4"
>
<input
name="name"
type="text"
placeholder="Your name"
required
class="w-full border p-2 rounded"
/>
<input
name="email"
type="email"
placeholder="Your email"
required
class="w-full border p-2 rounded"
/>
<textarea
name="message"
placeholder="Your message"
required
class="w-full border p-2 rounded h-32"
></textarea>
<button
type="submit"
class="rounded bg-black text-white px-4 py-2 hover:bg-neutral-800 transition"
>
Send
</button>
</form>
A few good practices: • Always validate fields on both the client and the server. • Never expose the API key: everything goes through your Astro endpoint. • Add visual state handling (loading, success, error) if you want to improve the experience.
5. Going a bit further
Once the basics are in place, the system is easy to extend:
• Build a nicer HTML template for the email you receive.
• Add a light anti-spam check (a hidden field, or an hCaptcha token).
• Use a dedicated subdomain for sending (for example mail.mydomain.dev).
Conclusion
Setting up email sending in Astro with Resend is both quick and clean. You keep full control over your code and your data, without fighting a mail server or a heavyweight service.
The Astro + Resend combination is ideal for a marketing site or a portfolio: • fast integration, • minimal code, • reliable delivery, • and almost no maintenance.
It’s the kind of detail an end user never sees, and that changes everything in the quality of a delivered project.
Discover Resend