This episode covers forms and interactions in Astro: form handling on static and server pages, client-side interactions with hydrated components, UI validation and progressive enhancement, and form submission through serverless functions or endpoints.

A form is the bridge between visitors and site owners: newsletters, contact, signups, or search. Episode 10 covers how to build forms in Astro with the right patterns — from a simple HTML form to an interactive one that sends data to a serverless function.
The key to this chapter is progressive enhancement: a form must keep working even if JavaScript fails to load. You will learn to build forms that work in two layers — pure HTML for the base, and JavaScript interaction for a smoother experience.
This episode also covers UI validation, feedback for users, and how to connect forms to serverless endpoints.
The most basic form needs no JavaScript at all. Use the standard HTML action and method attributes:
<form action="/api/pendaftaran" method="POST">
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<button type="submit">Daftar</button>
</form>The form above sends data with method="POST" to /api/pendaftaran. This is the foundation of progressive enhancement — even without a single line of JavaScript, the form still works.
To receive form submissions, create an endpoint in src/pages/api. In server mode, you can write a handler with the post function:
export async function post({ request }) {
const formData = await request.formData();
const email = formData.get("email");
if (!email) {
return new Response("Email wajib diisi", { status: 400 });
}
return new Response("Terima kasih sudah mendaftar", { status: 200 });
}The endpoint above reads request.formData() from the form submission, validates the email, and returns a response. The request.formData() pattern is only available in server mode — in static mode you need a serverless function from your hosting platform.
For real-time feedback — validation while typing, loading indicators, or success messages without a reload — wrap the form in a hydrated component:
export default function FormDaftar() {
const [status, setStatus] = useState("idle");
async function kirim(event) {
event.preventDefault();
setStatus("loading");
const body = new FormData(event.currentTarget);
const res = await fetch("/api/pendaftaran", { method: "POST", body });
setStatus(res.ok ? "sukses" : "gagal");
}
return (
<form onSubmit={kirim}>
<input name="email" type="email" required />
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Mengirim..." : "Daftar"}
</button>
</form>
);
}This component is hydrated with client:load on the Astro page. The form uses fetch to send data without a reload, and shows a loading status while submitting.
Use validation in two places: HTML attributes such as required, type="email", and minlength on the browser side, then validate again on the server. HTML attributes are the first layer; server validation is the real layer — never rely only on JavaScript.
<input name="email" type="email" required minlength="5" />The type="email" and required attributes make the browser reject empty or incorrectly formatted input before the form is sent — without a single line of JavaScript.
In interactive components, show specific error messages near the field, not just an alert. When JavaScript is off, users still get a response from the server through a reloaded page with the standard action attribute.
As always in Astro: add JavaScript only if it genuinely adds value. A simple contact form may be fine with pure HTML. A signup form with real-time validation deserves a hydrated component.
For static sites, the /api endpoint is not available on your own server. The solution is a serverless function from the hosting platform. Example on Netlify:
# netlify/functions/pendaftaran.mjs
export default async (req) => {
const data = await req.json();
return new Response("OK", { status: 200 });
};Serverless functions run on the hosting platform's edge and can receive form submissions from static sites. You can use Vercel Functions, Netlify Functions, or Cloudflare Workers depending on your hosting.
Form data must go somewhere: email, a database, or a third-party service like Formspree. Choose a service that matches your load and privacy needs — this decision also relates to the security discussed in episode 12.
Warning
Never put data-storage logic directly in a client component. Sensitive data must pass through a server endpoint or a serverless function for validation and protection.
Episode 10 builds the bridge between visitors and site owners: pure HTML forms that work without JavaScript, API endpoints in server mode, interactive form components with hydration, two-layer validation with progressive enhancement, and form submission through serverless functions.
The key takeaways:
action and method work without JavaScript.src/pages/api receive submissions with request.formData().In the next episode 11, we will cover content management and CMS: integrating headless CMS platforms like Contentful, Sanity, or Strapi, content sourcing from Git-based CMS, preview mode and build-time content updates, and authoring workflows and metadata management. Your content will be managed by a team like a real production.