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!
Connecting Custom Forms to Google Spreadsheets with Apps Script đ
Turn any Google Spreadsheet into a free form backend with Google Apps Script â no server needed, with TypeScript type safety and CORS solutions.
Mutation Observer: The invisible force in the background of your website đ”ïž
The Mutation Observer is your invisible helper in the background, monitoring DOM changes in real time. Learn how to use it! đ”ïž
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: A powerful tool for efficient web design đ
Discover how the Intersection Observer makes your websites more efficient and why it should be an indispensable tool in your arsenal! đ