Plain HTML Forms — Server Handles the POST
Forms
An HTML form posts to an Astro endpoint. The endpoint reads FormData and responds — no client JavaScript needed.
What you'll learn
- Wire a form to a POST endpoint
- Read `FormData` on the server
- Show validation errors
Forms in Astro can be as old-fashioned or modern as you like.
Server mode lets you handle a POST with zero client JavaScript.
The HTML Side
---
// src/pages/contact.astro
export const prerender = false;
let submitted = false;
let error = "";
if (Astro.request.method === "POST") {
const data = await Astro.request.formData();
const email = data.get("email");
const message = data.get("message");
if (typeof email !== "string" || !email.includes("@")) {
error = "Please enter a valid email.";
} else {
await saveContact({ email, message });
submitted = true;
}
}
---
{submitted ? (
<p>Thanks! We'll be in touch.</p>
) : (
<form method="POST">
{error && <p class="error">{error}</p>}
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button>Send</button>
</form>
)} The same page handles both GET (show the form) and POST (process the submission). No fetch, no client JS, fully works without JavaScript.
Or — Submit to an Endpoint
If you’d rather keep the page static and have the form submit to a JSON API:
<form method="POST" action="/api/contact">
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button>Send</button>
</form> // src/pages/api/contact.ts
export const prerender = false;
export const POST: APIRoute = async ({ request, redirect }) => {
const data = await request.formData();
await saveContact({
email: data.get("email"),
message: data.get("message"),
});
return redirect("/contact?thanks=1", 303);
}; Return a 303 redirect (Post/Redirect/Get pattern) to prevent double-submits on refresh.
Progressive Enhancement With JS
When JS loads, you can intercept the form for a smoother UX:
<form id="contact" method="POST" action="/api/contact">
...
</form>
<script>
const form = document.getElementById("contact") as HTMLFormElement;
form?.addEventListener("submit", async e => {
e.preventDefault();
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
});
if (res.ok) {
form.outerHTML = "<p>Thanks!</p>";
}
});
</script> The form works without JS; with JS it gets a no-reload submit. That’s progressive enhancement.
Up Next
A higher-level pattern Astro ships — Actions.
Actions →