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 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 oder neuer
- Ein Cloudflare-Konto (D1, R2, Turnstile, alle mit kostenlosem Kontingent)
- Ein Resend-Konto zum Versenden von E-Mails
Warum Astro Actions für Formulare
Vor Actions bedeutete die Verarbeitung eines Formulars in Astro, einen API-Endpunkt 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 Sieactions.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:
- Öffnen Sie im Cloudflare-Dashboard Application Security > Turnstile und wählen Sie Add widget manually.
- Geben Sie ihm einen Namen und fügen Sie Ihre Domains hinzu, einschließlich
localhostfür die lokale Entwicklung. - Belassen Sie den Modus auf Managed, sodass Cloudflare entscheidet, wann ein Besucher herausgefordert wird.
- 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:
npm create astro@latest my-astro-forms-appnpm 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:
cd my-astro-forms-appDen 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.
npx astro add cloudflareDas richtet astro.config.mjs ein. Dieses Beispiel hält es minimal:
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:
npx wrangler d1 create formsnpx wrangler r2 bucket create forms-uploadswrangler 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:
{ "$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):
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:
PUBLIC_TURNSTILE_SITE_KEY="1x00000000000000000000AA"Erstellen Sie schließlich die Tabelle waitlist:
-- schema.sqlCREATE TABLE IF NOT EXISTS waitlist ( email TEXT PRIMARY KEY, created_at TEXT NOT NULL);npx wrangler d1 execute forms --remote --file=./schema.sqlDie 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:
npx wrangler typesDas 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:
npm install resendEin 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:
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 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:
/** * 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:
// 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 emailDie 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:
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.
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.
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.
---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 ActionErrors (die Nachricht anzeigen).
Lokal ausführen
Starten Sie den Entwicklungsserver:
npm run devDa 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, 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:
npx wrangler d1 execute forms --remote --file=./schema.sqlnpx wrangler secret put RESEND_API_KEYnpx wrangler secret put TURNSTILE_SECRET_KEYnpx wrangler secret put CONTACT_FROM_EMAIL
npm run build && npx wrangler deployTauschen Sie die Turnstile-Test-Keys gegen Ihre echten Site- und Secret-Keys aus, richten Sie CONTACT_FROM_EMAIL auf eine verifizierte Resend-Domain und Ihre Formulare laufen am Edge.
Referenzen
- GitHub-Repository
- Astro-Actions-Dokumentation
- Serverseitige Turnstile-Validierung
- Resend-Node.js-SDK
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.
Wenn Sie Fragen oder Kommentare haben, können Sie mich gerne auf Twitter erreichen.