---
title: "Typsichere Formularverarbeitung in Astro mit Actions: Validierung, Spam-Schutz und Datei-Uploads"
description: "Erstellen Sie produktionsreife Kontakt-, Wartelisten- und Datei-Upload-Formulare in Astro mit Actions: serverseitige Zod-Validierung, Turnstile-Bot-Schutz, Cloudflare D1 und R2 sowie Resend, ganz ohne separates Backend."
source: "https://www.launchfa.st/de/blog/astro-actions-forms"
author: "Rishi Raj Jain"
created_at: 2026-07-27T00:00:00.000Z
updated_at: 2026-07-27T00:00:00.000Z
---

> **Site index for agents**
> Fetch https://www.launchfa.st/llms.txt to discover every page on this site.
> Every page is also available as Markdown by appending `.md` to its URL.

<img width="1600" height="900" alt="Typsichere Formularverarbeitung in Astro mit Actions" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/astro-actions-forms.png" />

Fast jede Website braucht ein Formular (ein Kontaktfeld, eine Warteliste, einen Datei-Upload), und schon wächst ein Formular zu einem Backend heran. Sie beginnen mit einem HTML-`<form>`, dann brauchen Sie serverseitige Validierung, dann Spam-Schutz, dann einen Ort zum Speichern der Daten, dann eine transaktionale E-Mail.

[Astro Actions](https://docs.astro.build/en/guides/actions/) ermöglichen es Ihnen, eine typsichere Serverfunktion zu erstellen, die Sie direkt aus dem Browser wie eine lokale Funktion aufrufen. Astro übernimmt die Serialisierung, führt die Zod-Validierung auf dem Server aus und liefert Ihnen typisierte Ergebnisse und typisierte Fehler. In dieser Anleitung erstellen Sie drei Formulare mit Actions: ein Kontaktformular, eine Warteliste und einen Datei-Upload, die alle auf der Cloudflare-Workers-Laufzeit mit Turnstile, Cloudflare D1, Cloudflare R2 und Resend laufen.

## Voraussetzungen

Sie benötigen Folgendes:

- [Node.js 22](https://nodejs.org/en/blog/announcements/v22-release-announce) oder neuer
- Ein [Cloudflare-Konto](https://dash.cloudflare.com/sign-up) (D1, R2, Turnstile, alle mit kostenlosem Kontingent)
- Ein [Resend](https://resend.com)-Konto zum Versenden von E-Mails

## Warum Astro Actions für Formulare

Vor Actions bedeutete die Verarbeitung eines Formulars in Astro, einen [API-Endpunkt](https://docs.astro.build/en/guides/endpoints/) zu erstellen, `FormData` von Hand zu parsen, es manuell zu validieren und Typen zwischen Client und Server zu duplizieren. Actions ersetzen all das durch eine einzige Definition, die:

- **Durchgängig typsicher**: Eingabeschema und Rückgabewert werden auf beiden Seiten abgeleitet. Wenn Sie das Schema ändern, schlägt die Typprüfung des Client-Aufrufs fehl.
- **Serverseitig validiert**: Die Eingabe wird mit Zod (importiert aus `astro/zod`) geparst, bevor Ihr Handler ausgeführt wird, sodass ungültige Eingaben Ihre Logik nie erreichen.
- **Direkt aufrufbar**: Aus einem `<script>` rufen Sie `actions.contact(formData)` auf und erhalten `{ data, error }` zurück. Validierungsfehler auf Feldebene kommen strukturiert zurück, sodass Sie sie inline anzeigen können.
- **Progressive-Enhancement-freundlich**: Actions akzeptieren natives `FormData`, sodass derselbe Handler funktioniert, egal ob Sie mit JavaScript absenden oder auf einen einfachen HTML-Formular-Post zurückfallen.

Der Reiz liegt in der Ergonomie eines Funktionsaufrufs bei gleichzeitiger Sicherheit eines echten Backends.

## Cloudflare Turnstile einrichten

Turnstile ist Cloudflares kostenlose, datenschutzfreundliche CAPTCHA-Alternative, die Bots und Spam aus Ihren Formularen fernhält, ohne echte Nutzer Rätsel lösen zu lassen.

Richten Sie es ein, bevor Sie Code schreiben, denn Sie brauchen die Schlüssel gleich:

1. Öffnen Sie im [Cloudflare-Dashboard](https://dash.cloudflare.com) **Application Security > Turnstile** und wählen Sie **Add widget manually**.
2. Geben Sie ihm einen Namen und fügen Sie Ihre Domains hinzu, einschließlich `localhost` für die lokale Entwicklung.
3. Belassen Sie den Modus auf **Managed**, sodass Cloudflare entscheidet, wann ein Besucher herausgefordert wird.
4. Kopieren Sie die beiden generierten Schlüssel: den **Site Key** (öffentlich, im Browser verwendet) und den **Secret Key** (privat, für die serverseitige Verifizierung).

Halten Sie beide für die Umgebungsdateien später bereit.

## Eine neue Astro-Anwendung erstellen

Legen wir los und erstellen ein neues Astro-Projekt. Führen Sie den folgenden Befehl aus:

```bash
npm create astro@latest my-astro-forms-app
```

`npm create astro` ist der empfohlene Weg, um schnell ein Astro-Projekt zu erstellen.

Wählen Sie bei den Abfragen:

- `Empty`, wenn Sie gefragt werden, wie Sie das neue Projekt starten möchten.
- `Yes`, wenn Sie gefragt werden, ob Sie TypeScript schreiben möchten.
- `Strict`, wenn Sie gefragt werden, wie streng TypeScript sein soll.
- `Yes`, wenn Sie gefragt werden, ob Abhängigkeiten installiert werden sollen.
- `Yes`, wenn Sie gefragt werden, ob ein Git-Repository initialisiert werden soll.

Wechseln Sie dann in das Projektverzeichnis:

```bash
cd my-astro-forms-app
```

## Den Cloudflare-Adapter für SSR hinzufügen

Actions werden bei Bedarf (on demand) gerendert, daher benötigt das Projekt den Cloudflare-Adapter. Er stellt außerdem D1, R2 und Turnstile in derselben Workers-Laufzeit bereit.

```bash
npx astro add cloudflare
```

Das richtet `astro.config.mjs` ein. Dieses Beispiel hält es minimal:

```js
// astro.config.mjs
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'

export default defineConfig({
  adapter: cloudflare({
    imageService: 'passthrough',
  }),
})
```

## Cloudflare D1 und R2 binden

Erstellen Sie den Speicher, den die Formulare benötigen: eine D1-Datenbank für die Warteliste und einen R2-Bucket für Uploads:

```bash
npx wrangler d1 create forms
npx wrangler r2 bucket create forms-uploads
```

`wrangler d1 create` und `wrangler r2 bucket create` fügen die Bindings (und die `database_id`) für Sie zu `wrangler.jsonc` hinzu und erstellen die Datei, falls sie nicht existiert. Das Ergebnis sieht so aus:

```jsonc
// wrangler.jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "my-astro-forms-app",
  "compatibility_date": "2026-07-27",
  "compatibility_flags": ["global_fetch_strictly_public"],
  "main": "@astrojs/cloudflare/entrypoints/server",
  "assets": { "directory": "./dist", "binding": "ASSETS" },
  "observability": { "enabled": true },
  "d1_databases": [
    {
      "binding": "forms",
      "database_name": "forms",
      "database_id": "<your-d1-database-id>",
      "remote": true
    }
  ],
  "r2_buckets": [
    {
      "bucket_name": "forms-uploads",
      "binding": "forms_uploads",
      "remote": true
    }
  ]
}
```

Das Flag `remote: true` weist Wrangler an, Ihre echten D1- und R2-Ressourcen auch während `astro dev` zu verwenden, führen Sie also einmal `npx wrangler login` aus, bevor Sie starten.

Geheimnisse bleiben außerhalb von `wrangler.jsonc`. Legen Sie sie für die lokale Entwicklung in `.dev.vars` ab (Wrangler lädt die Datei automatisch während `astro dev`):

```bash
# .dev.vars
RESEND_API_KEY="re_..."
CONTACT_FROM_EMAIL="Acme <onboarding@resend.dev>"
# Turnstile "always passes" test secret (swap for your real secret in production).
TURNSTILE_SECRET_KEY="1x0000000000000000000000000000000AA"
```

Und legen Sie den öffentlichen Turnstile-Site-Key in `.env` ab, damit er in den Client eingebettet werden kann:

```bash
# .env
PUBLIC_TURNSTILE_SITE_KEY="1x00000000000000000000AA"
```

Erstellen Sie schließlich die Tabelle `waitlist`:

```sql
-- schema.sql
CREATE TABLE IF NOT EXISTS waitlist (
  email TEXT PRIMARY KEY,
  created_at TEXT NOT NULL
);
```

```bash
npx wrangler d1 execute forms --remote --file=./schema.sql
```

## Die Laufzeitumgebung typisieren

Actions lesen Bindings und Geheimnisse aus dem Modul `cloudflare:workers`. Generieren Sie die Typen dafür direkt aus Ihrer `wrangler.jsonc`, damit `env` vollständig typisiert ist:

```bash
npx wrangler types
```

Das schreibt eine `worker-configuration.d.ts`, die Ihre `forms`- und `forms_uploads`-Bindings zusammen mit den Geheimnissen beschreibt. Installieren Sie nun Resend und Sie können mit dem Schreiben von Actions beginnen:

```bash
npm install resend
```

## Ein typsicheres Kontaktformular erstellen

Actions werden in `src/actions/index.ts` definiert und aus einem einzigen `server`-Objekt exportiert. Beginnen wir mit dem Kontaktformular.

### Die Kontakt-Action definieren

Jede Action deklariert, wie sie Eingaben akzeptiert (`'form'` für `FormData`), ein `input`-Zod-Schema und einen `handler`. Das Schema ist die Validierung: Was nicht passt, erreicht Ihren Handler nie, und der Fehler wird als strukturierte Feldfehler an den Client zurückgegeben. Bindings und Geheimnisse stammen aus dem `env` von `cloudflare:workers`, und der Resend-Client wird einmalig im Modul-Scope erstellt:

```ts
// src/actions/index.ts
import { ActionError, defineAction } from 'astro:actions'
import { z } from 'astro/zod'
import { Resend } from 'resend'
import { env } from 'cloudflare:workers'

const resend = new Resend(env.RESEND_API_KEY)

export const server = {
  contact: defineAction({
    accept: 'form',
    input: z.object({
      name: z.string().trim().min(1, 'Please enter your name.'),
      email: z.string().trim().email('Enter a valid email address.'),
      message: z.string().trim().min(10, 'Your message should be at least 10 characters.'),
      // Injected by the Turnstile widget as a hidden field.
      'cf-turnstile-response': z.string().min(1, 'Please complete the anti-bot check.'),
    }),
    handler: async (input, context) => {
      // ...verify Turnstile, then send the email (below)
      return { ok: true }
    },
  }),
}
```

### Bots mit Turnstile blockieren

Ein öffentliches Kontaktformular ist ein Spam-Magnet. [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) bietet Ihnen eine datenschutzfreundliche CAPTCHA-Alternative. Das Widget fügt dem Formular ein `cf-turnstile-response`-Token hinzu, das Sie auf dem Server verifizieren. Fügen Sie eine kleine Hilfsfunktion hinzu und rufen Sie sie zuerst im Handler auf:

```ts
/**
 * Verify a Cloudflare Turnstile token server-side.
 * Docs: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
 */
async function verifyTurnstile(token: string, secret: string, ip?: string) {
  const body = new FormData()
  body.append('secret', secret)
  body.append('response', token)
  if (ip) body.append('remoteip', ip)

  const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body,
  })
  const data = (await res.json()) as { success: boolean }
  return data.success
}
```

Mit `ActionError` können Sie mit einem passenden HTTP-artigen Code fehlschlagen, den der Client von einem Validierungsfehler unterscheiden kann:

```ts
// inside the contact handler:
const ip = context.request.headers.get('CF-Connecting-IP') ?? undefined

const passed = await verifyTurnstile(input['cf-turnstile-response'], env.TURNSTILE_SECRET_KEY, ip)
if (!passed) {
  throw new ActionError({
    code: 'FORBIDDEN',
    message: 'Anti-bot verification failed. Please try again.',
  })
}
// ...send email
```

### Die E-Mail mit Resend versenden

Wenn die Anfrage validiert und verifiziert ist, stellen Sie sie zu. Das Resend-SDK läuft problemlos auf der Workers-Laufzeit:

```ts
const { error } = await resend.emails.send({
  from: env.CONTACT_FROM_EMAIL,
  to: input.email,
  subject: `New contact message from ${input.name}`,
  text: `From: ${input.name} <${input.email}>\n\n${input.message}`,
})
if (error) {
  throw new ActionError({
    code: 'INTERNAL_SERVER_ERROR',
    message: 'Could not send your message. Please try again later.',
  })
}

return { ok: true }
```

Hier geht die Bestätigung an die im Formular angegebene Adresse (`input.email`). Um Kontaktnachrichten stattdessen an Ihren eigenen Posteingang zu leiten, senden Sie `to` an Ihre Adresse und setzen `replyTo: input.email`.

## Eine Warteliste mit Cloudflare D1

Die Warteliste speichert E-Mails in D1 über das `env.forms`-Binding. Das interessante Detail ist die Idempotenz: Da `email` der Primärschlüssel ist, löst eine erneute Anmeldung einen `UNIQUE`-Constraint-Fehler aus, den wir als Erfolg behandeln.

```ts
joinWaitlist: defineAction({
  accept: 'form',
  input: z.object({
    email: z.string().trim().toLowerCase().email('Enter a valid email address.'),
  }),
  handler: async (input, context) => {
    try {
      await env.forms
        .prepare('INSERT INTO waitlist (email, created_at) VALUES (?, ?)')
        .bind(input.email, new Date().toISOString())
        .run()
    } catch (err) {
      // A UNIQUE violation just means they already signed up, so treat it as success.
      if (!String(err).includes('UNIQUE')) {
        throw new ActionError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Could not join the waitlist. Please try again.',
        })
      }
    }

    await resend.emails.send({
      from: env.CONTACT_FROM_EMAIL,
      to: input.email,
      subject: "You're on the waitlist 🎉",
      text: "Thanks for joining! We'll email you the moment we launch.",
    })

    return { ok: true, email: input.email }
  },
}),
```

Beachten Sie `z.string().trim().toLowerCase()`. Zod normalisiert die Eingabe als Teil der Validierung, sodass `Ada@Example.com ` und `ada@example.com` keine zwei Zeilen erzeugen.

## Datei-Uploads zu Cloudflare R2 verarbeiten

Bei selbstgebauten Uploads müssen Sie Multipart-Bodies parsen, die Datei validieren und sie in den Speicher streamen. Mit Actions kommt die Datei als standardmäßiges `File` an, und Sie validieren sie mit demselben Zod-Schema-Stil wie alles andere. Der R2-Bucket ist das `env.forms_uploads`-Binding.

```ts
upload: defineAction({
  accept: 'form',
  input: z.object({
    file: z
      .instanceof(File, { message: 'Please choose a file to upload.' })
      .refine((f) => f.size > 0, 'The selected file is empty.')
      .refine((f) => f.size <= 5 * 1024 * 1024, 'File must be 5 MB or smaller.')
      .refine(
        (f) => ['image/png', 'image/jpeg', 'application/pdf'].includes(f.type),
        'Only PNG, JPEG, or PDF files are allowed.',
      ),
  }),
  handler: async (input, context) => {
    const ext = input.file.name.split('.').pop()?.toLowerCase() ?? 'bin'
    const key = `uploads/${crypto.randomUUID()}.${ext}`

    await env.forms_uploads.put(key, await input.file.arrayBuffer(), {
      httpMetadata: { contentType: input.file.type },
    })

    return { ok: true, key, size: input.file.size }
  },
}),
```

Typ und Größe *vor* dem Zugriff auf R2 zu validieren bedeutet, dass Sie einen 4-GB-Upload oder eine getarnte ausführbare Datei am Rand Ihres Handlers ablehnen.

## Die Formulare im Client verdrahten

Nun das Frontend. Laden Sie das Turnstile-Skript, rendern Sie die drei Formulare und rufen Sie die Actions aus einem `<script>` auf. Der Client-Import `astro:actions` liefert Ihnen ein typisiertes `actions`-Objekt und eine `isInputError`-Hilfsfunktion, um Validierungsmeldungen auf Feldebene herauszuziehen.

```astro
---
// src/pages/index.astro
const siteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY
---

<form id="contact">
  <h2>Contact</h2>
  <label>Name <input name="name" required /></label>
  <label>Email <input name="email" type="email" required /></label>
  <label>Message <textarea name="message" rows="4" required></textarea></label>
  <div class="cf-turnstile" data-sitekey={siteKey}></div>
  <button type="submit">Send message</button>
  <p class="status" role="status"></p>
</form>

<form id="waitlist">
  <h2>Join the waitlist</h2>
  <label>Email <input name="email" type="email" required /></label>
  <button type="submit">Join</button>
  <p class="status" role="status"></p>
</form>

<form id="upload">
  <h2>Upload a file</h2>
  <label>File (PNG, JPEG, or PDF, max 5 MB) <input name="file" type="file" required /></label>
  <button type="submit">Upload</button>
  <p class="status" role="status"></p>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" />

<script>
  import { actions, isInputError } from 'astro:actions'

  const forms: Record<string, (data: FormData) => Promise<{ data?: { ok: boolean }; error?: any }>> = {
    contact: actions.contact,
    waitlist: actions.joinWaitlist,
    upload: actions.upload,
  }

  for (const [id, action] of Object.entries(forms)) {
    const form = document.getElementById(id) as HTMLFormElement | null
    if (!form) continue
    const status = form.querySelector('.status') as HTMLParagraphElement

    form.addEventListener('submit', async (event) => {
      event.preventDefault()
      status.textContent = 'Submitting…'

      const { data, error } = await action(new FormData(form))

      if (error) {
        // Field-level validation errors come back typed from the Zod schema.
        const message = isInputError(error)
          ? Object.values(error.fields).flat()[0] ?? 'Please check the form and try again.'
          : error.message
        status.textContent = message
        return
      }

      status.textContent = data?.ok ? 'Done ✅' : 'Submitted.'
      form.reset()
      // @ts-expect-error global injected by the Turnstile script
      window.turnstile?.reset?.()
    })
  }
</script>
```

Die entscheidende Zeile ist `await action(new FormData(form))`. Es gibt keinen `fetch`-Aufruf und keinen Endpunkt zu schreiben. Sie rufen die Action mit den Formulardaten auf und erhalten ein typisiertes `{ data, error }` zurück. `isInputError` unterscheidet Zod-Validierungsfehler (inline anzeigen) von geworfenen `ActionError`s (die Nachricht anzeigen).

## Lokal ausführen

Starten Sie den Entwicklungsserver:

```bash
npm run dev
```

Da die Bindings mit `remote: true` markiert sind, spricht die Entwicklungsumgebung mit Ihren echten D1- und R2-Ressourcen (stellen Sie sicher, dass Sie `npx wrangler login` ausgeführt haben). Öffnen Sie [localhost:4321](http://localhost:4321), senden Sie das Kontaktformular ab (der Turnstile-Test-Key wird automatisch akzeptiert), tragen Sie sich in die Warteliste ein und laden Sie eine Datei hoch.

## Auf Cloudflare deployen

`wrangler.jsonc` verweist mit `main` bereits auf den Server-Einstiegspunkt des Adapters und mit `assets` auf `./dist`, sodass das Deployment aus einem Build gefolgt von `wrangler deploy` besteht. Wenden Sie zuerst das Schema an und übertragen Sie Ihre Geheimnisse:

```bash
npx wrangler d1 execute forms --remote --file=./schema.sql
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put TURNSTILE_SECRET_KEY
npx wrangler secret put CONTACT_FROM_EMAIL

npm run build && npx wrangler deploy
```

Tauschen Sie die Turnstile-Test-Keys gegen Ihre echten [Site- und Secret-Keys](https://developers.cloudflare.com/turnstile/get-started/) aus, richten Sie `CONTACT_FROM_EMAIL` auf eine [verifizierte Resend-Domain](https://resend.com/docs/dashboard/domains/introduction) und Ihre Formulare laufen am Edge.

## Referenzen

- [GitHub-Repository](https://github.com/rishi-raj-jain/my-astro-forms-app)
- [Astro-Actions-Dokumentation](https://docs.astro.build/en/guides/actions/)
- [Serverseitige Turnstile-Validierung](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
- [Resend-Node.js-SDK](https://resend.com/docs/send-with-nodejs)

## Fazit

In dieser Anleitung haben Sie drei produktionsnahe Formulare in Astro erstellt (Kontakt, Warteliste und Datei-Upload) und dabei Actions für typsichere, serverseitig validierte Logik verwendet, mit Turnstile für Spam-Schutz, Cloudflare D1 und R2 für die Speicherung und Resend für E-Mails. Das Muster skaliert: Jedes Formular, das Sie später hinzufügen, ist nur ein weiterer Eintrag im `server`-Objekt, von Zod validiert und direkt vom Client aufrufbar, ohne separate API, die Sie erstellen oder pflegen müssen.

Wenn Sie lieber von einer sofort einsatzbereiten SaaS-Grundlage starten möchten, bei der Authentifizierung, Abrechnung, Speicherung und E-Mail bereits auf Astro und Cloudflare verdrahtet sind, werfen Sie einen Blick auf [LaunchFast](https://www.launchfa.st).

Wenn Sie Fragen oder Kommentare haben, können Sie mich gerne auf [Twitter](https://twitter.com/direct_messages/create/rishi_raj_jain_) erreichen.
