# Astro Actions or a form backend?

We built one Astro contact form with Astro Actions on Cloudflare and with a form backend on a static build: what each needed, what broke, which to pick.

Author: Formgong
Published: 2026-11-02
Language: en
Canonical: https://formgong.com/en/blog/astro-actions-vs-form-backend/

Astro Actions run on a server, so they need an adapter (Cloudflare, Netlify, Vercel or Node) and a page rendered on demand. A static Astro build with an actions file will not build at all. You also write the sending code yourself and need an email service with a verified domain and a secret key. A form backend works the other way round: the site stays static, the form posts to the backend's URL, and the backend sends the email. Pick Actions when the site already runs on a server and the data must reach your own code; pick a backend for a plain contact form on a static site.

## Two ways to send an Astro form

Astro Actions are functions you write in `src/actions/index.ts`. Astro checks the input against a schema, runs your handler on the server and hands the result back to the page. For a contact form, the handler is where you call an email API such as Resend. The Astro docs are clear about one condition: “Pages must be on-demand rendered when calling actions using a form action.”

A form backend skips the server part. The form's `action` points at the backend's URL, the browser posts the fields there, and the backend emails them to you. Nothing runs in your Astro project, so the build can stay fully static.

We built the same small contact form both ways with Astro 7 and the Cloudflare adapter, then sent it from Chromium.

## What we built and what happened

Astro 7.4.4 and @astrojs/cloudflare 14.4.3, built and served with `astro preview`, form sent from Chromium, 2 November 2026.
What we built or sentWhat happened
Static build, an action and a page with `prerender = false`Build failed: `[NoAdapterInstalled] Cannot use server-rendered pages without an adapter.`
Static build, the actions file alone, no page using itBuild failed: `[ActionsWithoutServerOutputError] A server is required to create callable backend functions.`
Static build, no actions file, only the page with the Formgong formBuilt in 1.5 seconds: one 12 KB HTML file, no server code
Cloudflare adapter buildBuilt in 10–11 seconds; the server bundle was 2.3 MB
Action, valid input, wrong Resend keyHTTP 500, the page showed `Resend 401: API key is invalid`
Action, invalid email, browser check turned offHTTP 400, the page showed `Check the fields: email`
Static page, plain form posting to FormgongLanded on Formgong's thank-you page (formgong.com/submitted)
The second row was the surprise. We removed the page that used the action and kept only `src/actions/index.ts`, and the static build still stopped. Once that file exists, Astro wants a server whether or not a page calls it.

With the Cloudflare adapter everything worked as documented. A wrong email address came back as an input error with the field name, before our handler ran. The wrong Resend key came back as our own `ActionError` with Resend's message, “API key is invalid”. The plain form built as a normal static site and, once sent, landed on Formgong's thank-you page.

## The Astro Actions version

This is the code we ran, cleaned up for a real site. The action validates the fields with Zod, sends one email through Resend's API and throws an `ActionError` if Resend refuses. The page shows the result after the post.

```typescript
// src/actions/index.ts
import { defineAction, ActionError } from "astro:actions";
import { z } from "astro/zod";
import { RESEND_API_KEY } from "astro:env/server";

export const server = {
  send: defineAction({
    accept: "form",
    input: z.object({ email: z.email(), message: z.string().min(1) }),
    handler: async ({ email, message }) => {
      const response = await fetch("https://api.resend.com/emails", {
        method: "POST",
        headers: { Authorization: `Bearer ${RESEND_API_KEY}`, "Content-Type": "application/json" },
        body: JSON.stringify({
          from: "Site <forms@your-domain.com>",
          to: ["you@your-domain.com"],
          reply_to: email,
          subject: "Contact form",
          text: message,
        }),
      });
      if (!response.ok) {
        const body = await response.json().catch(() => ({}));
        throw new ActionError({ code: "INTERNAL_SERVER_ERROR", message: `Resend ${response.status}: ${body.message}` });
      }
      return { sent: true };
    },
  }),
};
```

**The contact page**

```astro
---
// src/pages/contact.astro
export const prerender = false;
import { actions, isInputError } from "astro:actions";
const result = Astro.getActionResult(actions.send);
---
{result?.data?.sent && <p>Thanks! We'll reply soon.</p>}
{result?.error && <p>{isInputError(result.error) ? "Please check your email address." : "Not sent. Please try again."}</p>}
<form method="POST" action={actions.send}>
  <input name="email" type="email" required aria-label="Email" />
  <textarea name="message" required aria-label="Message"></textarea>
  <button>Send</button>
</form>
```

## The setup Actions need besides the code

The code is the short part. Before the form sent anything, we also needed the adapter in `astro.config.mjs`, a small Wrangler config (Astro can generate one), and the API key declared as a server secret in Astro's `env` schema. On Cloudflare the key goes in with `wrangler secret put`. Locally, the adapter guide says secrets go in a `.dev.vars` file. In our first run `astro preview` did not pass the key from `.dev.vars` to the action (our log said the key was missing). It did pick it up from an environment variable in the second run.

Then there is the email side. Resend's domain page says: “You must add and verify at least one domain to send emails with Resend.” That means DNS records on your domain, and the same is true of most email APIs. Our browser test also showed a small trap. The browser's own check on `type="email"` stopped the bad address before it reached the server, so the server-side check only ran once we turned the browser check off. Bots and scripts skip the browser, which is why the schema is still worth having.

## The form backend version

This needs no adapter and no secret, and it keeps `output: 'static'`. Replace the key with your own and the form sends. Formgong adds a spam filter, the honeypot field shown here, an inbox, and email or Telegram delivery. The [Astro setup page](/en/for/astro/) also shows how to display the answer without leaving the page.

```html
<form method="POST" action="https://formgong.com/submit">
  <input type="hidden" name="access_key" value="fk_your_access_key" />
  <input type="text" name="botcheck" style="display:none" tabindex="-1" autocomplete="off" />
  <input name="email" type="email" required aria-label="Email" />
  <textarea name="message" required aria-label="Message"></textarea>
  <button>Send</button>
</form>
```

## Which one to pick

Use Actions when your site already runs on an adapter, or the submission has to reach your own code: you save it to your database, check it against a logged-in user, or start something in your app. You get typed input, one place for the logic, and no outside service between the visitor and you.

For a contact or quote form on a static site, a backend is less work. In our test the only change was the form's `action` and one hidden field. With Actions it was an adapter, a server deployment, a secret, and an email account with a verified domain. To try the backend, create a form on the [Formgong home page](/en/) and paste its key.

## Running Actions on another adapter?

If you tried this on Netlify, Vercel or the Node adapter and something behaved differently, tell us at [support@formgong.com](mailto:support@formgong.com). We will run it and add a row.

## What Formgong's free plan includes

- **Build the form or bring your own.** The [form builder](/en/tools/form-builder/) takes the colours and fonts from your site's address, with no sign-up; ready [form templates](/en/templates/) copy into your account; AI writes the fields from one sentence. Any hand-written HTML form works too.

- **Websites with the form already connected.** 28 free [website templates](/en/website-templates/) post to Formgong out of the box.

- **250 submissions a month on unlimited forms.** Spam does not count. Once a year, a busy month keeps delivering on credit until the 7th instead of stopping.

- **Everywhere you work.** Email to up to 3 people at once, [Telegram](/en/integrations/telegram/) with Reply and Handled buttons, [Google Sheets](/en/integrations/google-sheets/), [Notion](/en/integrations/notion/), and 3 signed [webhooks](/en/integrations/webhooks/) for Slack, Discord, Zapier, Make or n8n.

- **Emails to your customers in your business's name.** Formgong sends them under your business name and replies come back to you, with no email service of your own to set up: a review request to every lead, a reminder a visitor asked for, and review emails after an order with your logo and colours. People who agree can also go straight into your [Mailchimp, Klaviyo, Brevo, MailerLite or Kit](/en/docs/marketing-lists/) list. An automatic reply to each visitor is on Pro.

- **No captcha needed.** A server-side spam filter, a honeypot, allowed domains and a block list; add your own Turnstile key if you want one.

- **Files and voice.** Visitors can attach files, or record a [voice message](/en/docs/voice-field/) that arrives transcribed.

- **Made for AI-built sites.** A hosted form page and a contact button; step-by-step guides for sites built with [Lovable](/en/docs/lovable/), [Bolt](/en/docs/bolt/), [v0](/en/docs/v0/), [Replit](/en/docs/replit/) and [Base44](/en/docs/base44/); an [MCP server](/en/docs/mcp/) lets Cursor or Claude wire the form for you.

- **See what the form does.** [Analytics](/en/docs/analytics/) for views, starts, conversion and where each lead came from, and an alert when a form goes quiet.

- **One account for the rest.** Customer reviews and an [AI help chat](/en/docs/help-chat/) that answers visitors from your own pages, with every message in one inbox. Data is stored in the EU.

## Frequently asked questions

### Do Astro Actions work on a static site?

No. In our test a static build failed as soon as src/actions/index.ts existed, even with no page using it. Actions need an adapter and an on-demand page.

### Can Astro Actions send email?

Only through code you write: the handler calls an email API such as Resend with a secret key, and the service needs a verified sending domain.

### How do I read a secret in an Astro Action on Cloudflare?

Declare it in the env schema of astro.config.mjs as a server secret and import it from astro:env/server. Set it with wrangler secret put in production.

### How do I send an Astro contact form without a server?

Point the form's action at a form backend URL. The site stays static and the backend emails the submission.

## Sources and documentation

- [Astro docs: Actions](https://docs.astro.build/en/guides/actions/)
- [Astro docs: Cloudflare adapter](https://docs.astro.build/en/guides/integrations-guide/cloudflare/)
- [Resend: Domains](https://resend.com/docs/dashboard/domains/introduction)
- [Astro](https://formgong.com/en/for/astro/)

[Get a form key](https://formgong.com/en/#top)
