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

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

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