From Form to Function: Building a Contact Form with Astro API Endpoints
One of Astro’s greatest advantages over other static-site generators is the flexibility. Officially, generators like Jekyll or Hugo do not support features like contact forms; you need a separate API or edge function for that. Astro, however, supports custom API endpoints that run on the edge.
Prerequisites
You’ll need:
- An Astro project (v5+)
- The Cloudflare adapter installed
npm create astro@latestnpx astro add cloudflareAstro endpoints
By default, Astro pre-renders all pages into static HTML. For server-side logic like form handling, you need to mark specific routes as dynamic.
Astro provides two approaches:
Static mode (default)
Most pages are pre-rendered. Add export const prerender = false to specific files that need server-side logic.
export const prerender = false;Server mode
All pages render at request time. Add export const prerender = true to static pages.
import cloudflare from '@astrojs/cloudflare';
import { defineConfig } from "astro/config";
export default defineConfig({
output: "server",
adapter: cloudflare(),
});For a contact form, stick with static mode. Only the API endpoint needs to be dynamic.
Building the contact form
1. Create the contact page
---
const contactFormAction = "/api/contact";
// This page can be pre-rendered (static HTML)
export const prerender = true;
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Contact Us</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
font-family: sans-serif;
padding: 2rem;
max-width: 600px;
margin: auto;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
input, textarea, button {
padding: 0.5rem;
font-size: 1rem;
}
button {
background-color: #333;
color: #fff;
border: none;
cursor: pointer;
}
button:hover {
background-color: #555;
}
</style>
</head>
<body>
<h1>Contact Us</h1>
<form method="POST" action={contactFormAction}>
<label>
Name:
<input type="text" name="name" required />
</label>
<label>
Email:
<input type="email" name="email" required />
</label>
<label>
Message:
<textarea name="message" rows="5" required></textarea>
</label>
<button type="submit">Send Message</button>
</form>
</body>
</html>2. Create the API endpoint
// This endpoint must run on the server
export const prerender = false;
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const formData = await request.formData();
const name = formData.get("name");
const email = formData.get("email");
const message = formData.get("message");
// Validate inputs
if (!name || !email || !message) {
return new Response("Missing required fields", { status: 400 });
}
// Here you could:
// - Send an email (via Resend, SendGrid, etc.)
// - Store in a database
// - Forward to a webhook (Slack, Discord)
// - Write to Cloudflare KV
console.log("Contact form submission:", { name, email, message });
// Redirect to thank-you page
return new Response(null, {
status: 302,
headers: {
Location: "/contact-success",
},
});
};3. Create a success page
---
export const prerender = true;
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Message Sent</title>
</head>
<body>
<h1>Thank you!</h1>
<p>Your message has been sent. We'll get back to you soon.</p>
<a href="/">← Back to home</a>
</body>
</html>Project structure
src/
├── pages/
│ ├── contact.astro # Static form page
│ ├── contact-success.astro # Static thank-you page
│ └── api/
│ └── contact.ts # Dynamic API endpoint
Testing locally
npm run dev
Navigate to http://localhost:4321/contact, fill out the form, and submit. Check your terminal for the logged output.
Deploying to Cloudflare
npm run buildnpx wrangler pages deploy ./dist --project-name=your-project-nameDebugging
- Server logs: Use
npx wrangler tailto monitor logs in real-time - Network tab: Check browser dev tools for the POST request
- Console: Server-side
console.logappears in the terminal, not the browser
Next steps
To make the form actually send emails, you could integrate:
You could also add client-side validation, CAPTCHA protection, or rate limiting for production use.