TL;DR: Mit Google Apps Script kannst du jedes Google Spreadsheet in ein kostenloses Formular-Backend verwandeln â ganz ohne Server, ohne Drittanbieter-Abo und mit voller TypeScript-Typsicherheit. Der einzige Haken: CORS. Aber auch dafĂźr gibtâs LĂśsungen.
Du brauchst ein Kontaktformular fĂźr deine Seite und dein erster Gedanke ist Formspree, Typeform oder ein eigener Backend-Service? Verständlich â aber hast du mal daran gedacht, einfach ein Google Spreadsheet als Backend zu nehmen? Klingt erstmal wild, funktioniert aber Ăźberraschend gut. In diesem Artikel zeige ich dir, wie du das Ganze aufsetzt.
đ¤ Warum ausgerechnet ein Spreadsheet?
Ganz ehrlich: FĂźr die meisten Kontaktformulare, Wartelisten oder Feedback-Sammler brauchst du kein volles Backend. Ein Spreadsheet bringt alles mit, was du brauchst:
- Kostenlos â kein Hosting, keine Abos, nada
- Sofort teilbar â Kollegen kĂśnnen die Einträge direkt im Sheet sehen
- Sortierung, Filter, Diagramme â alles schon eingebaut
- Kein Datenbank-Setup â das Spreadsheet ist deine Datenbank
âď¸ Schritt 1: Spreadsheet vorbereiten
Erstelle ein neues Google Spreadsheet und definiere in der ersten Zeile die Header, die zu deinen Formularfeldern passen:
| A | B | C | D |
|---|---|---|---|
| Timestamp | Name | Message |
Wichtig: Merk dir den Sheet-Namen (Standard ist "Sheet1") â den brauchst du gleich im Script.
đ Schritt 2: Das Apps Script aufsetzen
Jetzt wirdâs spannend. Ăffne dein Spreadsheet, geh auf Erweiterungen â Apps Script und ersetze den Default-Code durch Folgendes:
const SHEET_NAME = "Sheet1";
function doPost(e) {
const lock = LockService.getScriptLock();
lock.tryLock(10000);
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const data = JSON.parse(e.postData.contents);
const timestamp = new Date().toISOString();
sheet.appendRow([timestamp, data.name, data.email, data.message]);
return ContentService
.createTextOutput(JSON.stringify({ result: "success", row: sheet.getLastRow() }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", message: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
} finally {
lock.releaseLock();
}
}
// Optional: GET-Requests zum Testen
function doGet() {
return ContentService
.createTextOutput(JSON.stringify({ status: "ok", message: "Form endpoint is running" }))
.setMimeType(ContentService.MimeType.JSON);
}Was hier passiert, ist relativ straightforward: doPost(e) nimmt den JSON-Body entgegen, parst ihn und hängt eine neue Zeile ans Sheet. Der LockService sorgt dafßr, dass bei gleichzeitigen Requests keine Daten verloren gehen.
âď¸ Schritt 3: Als Web App deployen
- Klick auf Bereitstellen â Neue Bereitstellung
- Typ: Web-App
- AusfĂźhren als: Ich
- Zugriff: Jeder
- Bereitstellen â App autorisieren â URL kopieren
Die URL sieht dann so aus: https://script.google.com/macros/s/AKfycb.../exec
Achtung: Bei jeder Ănderung am Script musst du eine neue Bereitstellung erstellen oder die bestehende aktualisieren. Sonst laufen deine Requests weiter gegen die alte Version.
đť Schritt 4: Der TypeScript Form Handler
Jetzt kommt der Frontend-Teil. Hier ist ein sauberer, typsicherer Handler, den du in jedes Framework oder auch Vanilla JS/TS droppen kannst:
interface FormData {
name: string;
email: string;
message: string;
}
interface AppsScriptResponse {
result: "success" | "error";
row?: number;
message?: string;
}
const APPS_SCRIPT_URL = "https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec";
async function submitToSheet(data: FormData): Promise<AppsScriptResponse> {
const response = await fetch(APPS_SCRIPT_URL, {
method: "POST",
mode: "no-cors",
headers: {
"Content-Type": "text/plain",
},
body: JSON.stringify(data),
});
return { result: "success" };
}
const form = document.querySelector<HTMLFormElement>("#contact-form");
form?.addEventListener("submit", async (e) => {
e.preventDefault();
const formData: FormData = {
name: (document.getElementById("name") as HTMLInputElement).value,
email: (document.getElementById("email") as HTMLInputElement).value,
message: (document.getElementById("message") as HTMLTextAreaElement).value,
};
try {
await submitToSheet(formData);
form.reset();
alert("Erfolgreich gesendet!");
} catch (error) {
console.error("Senden fehlgeschlagen:", error);
alert("Etwas ist schiefgelaufen. Bitte versuche es erneut.");
}
});Dir fällt wahrscheinlich auf: mode: "no-cors" und Content-Type: "text/plain". Warum? Dazu kommen wir jetzt.
â ď¸ Das CORS-Problem â und drei Wege drumherum
Hier kommt der Teil, an dem die meisten scheitern: Google Apps Script kann keine CORS-Preflight-Requests (OPTIONS) verarbeiten. Das heiĂt konkret:
Content-Type: "application/json"lĂśst einen Preflight aus â Request schlägt fehlmode: "no-cors"funktioniert, aber du kannst den Response-Body nicht lesen
Je nach Use Case hast du drei Optionen:
Option A: no-cors (der pragmatische Weg)
Du schickst mit text/plain und no-cors. Die Daten kommen an, du bekommst nur keinen Response zurĂźck. FĂźr ein simples Kontaktformular ist das vĂśllig okay.
Option B: Redirect-Workaround
Falls du die Antwort brauchst: Sende Ăźber einen versteckten <iframe> oder nutze redirect: "follow". Etwas mehr Aufwand, aber machbar.
Option C: GET statt POST (mein Favorit fĂźr einfache Forms)
Bau das Apps Script so um, dass es GET mit Query-Parametern verarbeitet. Dann hast du keine CORS-Probleme und bekommst den Response:
function doGet(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const params = e.parameter;
sheet.appendRow([
new Date().toISOString(),
params.name,
params.email,
params.message,
]);
return ContentService
.createTextOutput(JSON.stringify({ result: "success" }))
.setMimeType(ContentService.MimeType.JSON);
}Und im Frontend wirdâs dann ganz simpel:
async function submitViaGet(data: FormData): Promise<AppsScriptResponse> {
const params = new URLSearchParams(data as Record<string, string>);
const response = await fetch(`${APPS_SCRIPT_URL}?${params}`);
return response.json();
}đ¨ Schritt 5: Das HTML-Formular
Das Formular selbst ist bewusst simpel gehalten â du kannst es nach Belieben stylen:
<form id="contact-form">
<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">Nachricht</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Senden</button>
</form>đ§ Bonus: Serverseitige Validierung
Klar, Client-Validierung ist nett â aber du willst dich nicht drauf verlassen. Hier eine erweiterte doPost-Variante mit Validierung:
function doPost(e) {
const lock = LockService.getScriptLock();
lock.tryLock(10000);
try {
const data = JSON.parse(e.postData.contents);
if (!data.email || !data.email.includes("@")) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", message: "UngĂźltige E-Mail" }))
.setMimeType(ContentService.MimeType.JSON);
}
if (!data.name || data.name.trim().length < 2) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", message: "Name ist erforderlich" }))
.setMimeType(ContentService.MimeType.JSON);
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
sheet.appendRow([new Date().toISOString(), data.name.trim(), data.email.trim(), data.message]);
return ContentService
.createTextOutput(JSON.stringify({ result: "success" }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", message: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
} finally {
lock.releaseLock();
}
}đ§ Bonus: E-Mail-Benachrichtigung bei neuen Einträgen
Du willst sofort wissen, wenn jemand das Formular abschickt? FĂźg einfach diese Zeile nach appendRow ein:
MailApp.sendEmail({
to: "[email protected]",
subject: `Neue Formular-Einreichung von ${data.name}`,
htmlBody: `
<h3>Neue Kontaktformular-Einreichung</h3>
<p><strong>Name:</strong> ${data.name}</p>
<p><strong>Email:</strong> ${data.email}</p>
<p><strong>Nachricht:</strong> ${data.message}</p>
`,
});đĄď¸ Was du bei der Sicherheit beachten solltest
Ein paar Dinge, die du im Hinterkopf behalten solltest:
- Formula Injection: Wenn ein Wert mit
=,+,-oder@anfängt, kann Google Sheets das als Formel interpretieren. Stelle solche Werte mit einem Apostroph (') voran - Rate Limiting: Apps Script hat eingebaute Quotas (~20.000 Aufrufe/Tag), aber fßr Spam-Schutz solltest du trotzdem ßber eigene Limits nachdenken
- Honeypot-Felder: FĂźg ein verstecktes Feld ein â wenn ein Bot es ausfĂźllt, lehnst du die Submission ab
- Keine sensiblen Daten lesen: Die Apps-Script-URL ist Ăśffentlich. Nutze sie nur zum Schreiben, nicht zum Lesen sensibler Daten
đĄ Fazit
Google Spreadsheets + Apps Script ist ein Setup, das ich fĂźr einfache Formulare mittlerweile wirklich gerne nutze. Kostenlos, kein Server-Overhead, und die Daten landen direkt in einem Sheet, das du mit deinem Team teilen kannst.
Der einzige Stolperstein ist CORS â aber mit der GET-Variante oder no-cors hast du das schnell im Griff. Der gesamte Code ist framework-agnostisch: React, Angular, Vue, Svelte oder plain HTML â alles kein Problem.
Mehr Artikel entdecken
CSS variables: Flexible styling for your components đ¨
CSS variables make your styling more flexible đŻ In this guide, I'll explain how to use, scope and override them in components! đ
Angular input() fĂźr Route-Parameter: Schluss mit ActivatedRoute đ
Mit Angular 16+ kannst du Route-Parameter direkt per input() in deine Komponente binden â ganz ohne ActivatedRoute. So geht's!
Vue.js: Moderner Ansatz fĂźr die Frontend-Entwicklung đ
Erfahre alles Ăźber Vue.js und wie du mit diesem leistungsstarken JavaScript-Framework beeindruckende Webanwendungen erstellen kannst. đ
jQuery ist tot: Warum Du es sofort aus Deinen Projekten entfernen solltest đ
Erfahre, warum jQuery in modernen Webprojekten oft nicht mehr nĂśtig ist und welche Alternativen Du nutzen solltest! đ
Mutation Observer: Die unsichtbare Kraft im Hintergrund deiner Webseite đľď¸
Der Mutation Observer ist dein unsichtbarer Helfer im Hintergrund, der DOM-Ănderungen in Echtzeit Ăźberwacht. Lerne, wie du ihn einsetzen kannst! đľď¸
Intersection Observer: Ein mächtiges Tool fĂźr effizientes Web-Design đ
Entdecke, wie der Intersection Observer deine Webseiten effizienter macht und warum er ein unverzichtbares Tool in deinem Arsenal sein sollte! đ