How to create a form in Astro (with Astro Actions)
Add a contact form to your Astro site with no backend. Copy-paste examples for a plain HTML form, a JavaScript submission with an inline thank-you, and the modern Astro Actions approach with server-side Zod validation.
Astro is a modern web framework for building fast, content-driven websites. Since Astro generates static HTML by default, it doesn’t include a built-in way to handle form submissions. FormBackend fills that gap by giving your Astro forms a backend endpoint that handles submissions, sends notifications, and stores the data for you.
In this guide, we’ll create a contact form in Astro and connect it to FormBackend so you can start collecting submissions without writing any server-side code. We’ll build a plain HTML form, submit it with JavaScript for an inline thank-you, then show the modern Astro Actions approach with server-side validation.
Create your form endpoint in FormBackend
First, create a FormBackend account if you don’t have one. Then create a new form and give it a name you can remember, for example “Astro Contact Form”. It can always be changed later and is only used for you to find your form.
After creating the form, go to the Set Up tab and copy the unique form URL. It will look something like https://www.formbackend.com/f/your-token-here. You’ll need this URL in a moment.
Create a new Astro app
If you already have an Astro site, skip to the next section. To create a new one, run:
npm create astro@latest
This will take you through a set of steps to create a new Astro app. Once the command finishes, cd into the directory you created your app in.
cd your-astro-app
Start the development server:
npm run dev
You can access your site on the URL shown in your terminal (usually http://localhost:4321).
Create the contact form page
Create a new file at src/pages/contact.astro. Astro uses .astro files for pages, which support a frontmatter section for imports and regular HTML for the template.
Add the following content:
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Contact us">
<h1>Contact us</h1>
<form method="POST" action="https://www.formbackend.com/f/your-token-here">
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Send message</button>
</form>
</Layout>
Replace your-token-here with the form token you copied from FormBackend. Visit /contact in your browser and you should see the form.
This is a standard HTML form. The action attribute points to your FormBackend endpoint and method="POST" tells the browser to send the data as a POST request. No JavaScript needed.
Style the form with CSS
You can add styles directly in the Astro component using a <style> tag. Add this below the closing </Layout> tag:
<style>
form {
max-width: 480px;
}
div {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 600;
}
input, textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
button {
background-color: #3498db;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
button:hover {
background-color: #2980b9;
}
</style>
Submit using JavaScript (optional)
The plain HTML form works perfectly, but if you want to avoid a page redirect after submission, you can submit the form with JavaScript using FormBackend’s AJAX support.
Add a <script> section to your Astro page:
<script>
const form = document.querySelector('form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const response = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { 'accept': 'application/json' },
});
if (response.ok) {
const result = await response.json();
form.innerHTML = `<p>${result.submission_text}</p>`;
} else {
form.insertAdjacentHTML('beforeend', '<p>Something went wrong — please try again.</p>');
}
});
</script>
This replaces the form with a thank-you message after successful submission, all without leaving the page.
Handle the form with Astro Actions
The two approaches above run entirely in the browser, which is perfect for a static site. If you want server-side validation and to keep your FormBackend URL off the client, Astro’s modern answer is Astro Actions. You define a typed action once and Astro wires up validation, progressive enhancement, and the result for you.
Actions run on the server, so the page needs on-demand rendering — add an
adapter and set
export const prerender = false on the page (or output: 'server' in your config).
Create src/actions/index.js:
import { defineAction } from 'astro:actions'; import { z } from 'astro:schema'; export const server = { submitContact: defineAction({ accept: 'form', input: z.object({ name: z.string().min(1, 'Please enter your name'), email: z.string().email('Please enter a valid email'), message: z.string().min(1, 'Please enter a message'), }), handler: async (input) => { const body = new FormData(); Object.entries(input).forEach(([key, value]) => body.append(key, value)); await fetch('https://www.formbackend.com/f/your-token-here', { method: 'POST', body, headers: { accept: 'application/json' }, }); return { success: true }; }, }), };
Then point the form’s action at it. Astro re-renders the page after the POST, so you read the
outcome with Astro.getActionResult and show validation messages inline:
---
import Layout from '../layouts/Layout.astro';
import { actions, isInputError } from 'astro:actions';
export const prerender = false;
const result = Astro.getActionResult(actions.submitContact);
const fieldErrors = isInputError(result?.error) ? result.error.fields : {};
---
<Layout title="Contact us">
{result && !result.error ? (
<p>Thanks! Your message has been sent.</p>
) : (
<form method="POST" action={actions.submitContact}>
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" />
{fieldErrors.name && <p role="alert">{fieldErrors.name[0]}</p>}
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" />
{fieldErrors.email && <p role="alert">{fieldErrors.email[0]}</p>}
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5"></textarea>
{fieldErrors.message && <p role="alert">{fieldErrors.message[0]}</p>}
</div>
<button type="submit">Send message</button>
</form>
)}
</Layout>
Because the action is set on the form’s action attribute, this works even before client
JavaScript loads — Astro handles the POST on the server, validates against your Zod schema, and
forwards valid submissions to FormBackend. Add client:load islands on top only if you want
fancier interactivity.
Add spam protection
FormBackend includes built-in spam filtering on all forms, but you can add an extra layer of protection using one of the supported CAPTCHA services:
- Cloudflare Turnstile (recommended, free and privacy-friendly)
- hCaptcha
- Google reCAPTCHA
You can also add a honeypot field. Just include a hidden input with name="_honeypot" and FormBackend will reject submissions that fill it in:
<input type="text" name="_honeypot" style="display:none" tabindex="-1" autocomplete="off">
Set up notifications
Once your form is working, configure how you want to be notified about new submissions:
- Email notifications: Get an email every time someone submits. Set this up on the Notifications tab in FormBackend.
- Auto-reply emails: Automatically send a confirmation email to the person who submitted.
- Third-party integrations: Forward submissions to Slack, Discord, Google Sheets, Notion, or any service via webhooks.
Next steps
- Read the getting started guide for a full overview of FormBackend features
- Customize the thank-you page or redirect to a URL after submission
- Check out guides for other frameworks: Next.js, React, Hugo, and more
- Comparing form services for your static site? See how FormBackend stacks up as a Web3Forms alternative
Frequently asked questions
Do I need a backend to handle forms in Astro?
No. Astro outputs static HTML by default and has no built-in form handling. Pointing your form's action at a FormBackend endpoint gives it a backend that stores submissions and emails you — no server code required.
How do I submit an Astro form without a page redirect?
Add a <script> tag that listens for the form's submit event, calls preventDefault(), and POSTs the data with fetch and the browser's FormData. Set the accept header to application/json so FormBackend returns JSON you can use to show an inline message. A full example is above.
Does this work with both static and server-rendered Astro?
Yes. Because the form posts directly to FormBackend, it works the same whether your Astro site is statically generated or server-rendered, and on any host.
How do I use Astro Actions to handle a form?
Define a server action with defineAction and an input schema (using astro:schema, which is Zod), then set the form's action attribute to the action. Astro validates the input on the server, runs your handler — which forwards the data to FormBackend — and you read the result with Astro.getActionResult. A full example is shown above. Actions need on-demand rendering, so add an adapter and disable prerendering on the page.
How do I add spam protection to an Astro form?
FormBackend filters spam automatically. You can add Cloudflare Turnstile, hCaptcha, or reCAPTCHA for more protection, or include a hidden honeypot field named _honeypot, which FormBackend will use to reject bot submissions.
How do I get notified when someone submits the form?
FormBackend emails you on every submission and stores them in your dashboard. You can also auto-reply to the sender and route submissions to Slack, Google Sheets, Notion, or any URL via webhooks.
Keep reading
How to add a form to your Gatsby site
Add a contact form to your Gatsby site with no backend. A complete React example that submits with fetch, shows an inline success message, and reports errors accessibly.
How to create a Svelte form (with validation)
Build a Svelte (SvelteKit) contact form with no backend. Copy-paste examples for a plain form, a JavaScript submission with success and error states, and a SvelteKit form action with server-side Zod validation.
How to create a Nuxt form (with validation)
Build a Nuxt contact form with no backend. Copy-paste examples for a plain form, a Vue 3 script-setup submission, validation with Nuxt UI and a Zod schema, and a Nuxt server route that keeps your endpoint private.
Add a form backend to your site in minutes
Connect any HTML form to FormBackend and start collecting submissions — no backend code required.
Start free