Forms

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.

4 min read Level 2/5 #astro#forms#post
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 →