Back to blog
guides

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.

J
Jesper Christiansen
7 min read

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:

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:

Next steps

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.

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