Static sites do not need a monthly form backend.
If you build portfolio sites, landing pages, or simple client websites, the contact form is usually the awkward bit. You do not want to run a server just to collect a name, email, and message. But many hosted form services charge $10-20/month once you want Google Sheets, notifications, or no branding.
A small Google Apps Script can do the same job for free.
The architecture
The flow is simple:
HTML form -> Google Apps Script doPost -> Google Sheet
-> email notification
-> optional auto-reply
Your form sends a POST request to a Google Apps Script web app. The script validates the submission, filters obvious spam, writes a row to Google Sheets, and sends an email notification with MailApp.sendEmail().
No server. No database. No SMTP provider. No monthly invoice.
Core handler
The endpoint is a doPost function:
function doPost(e) {
try {
return handlePost(e);
} catch (err) {
console.error(err);
return jsonResponse('error', 'Internal server error.');
}
}
Inside handlePost, the useful pipeline is:
- Parse JSON or form-urlencoded input.
- Check a honeypot field.
- Validate required fields.
- Reject malformed email addresses.
- Apply simple keyword spam filtering.
- Rate limit repeated submissions.
- Append a row to Google Sheets.
- Send the owner an email notification.
- Optionally send an auto-reply.
A honeypot field is especially useful. Bots often fill every field they see. Humans never fill a hidden _gotcha field. If that field has a value, return a fake success and do not write to the sheet.
Google Sheets as the database
For a contact form, Google Sheets is a perfectly reasonable database. It is searchable, exportable, and easy for non-technical clients to understand.
function ensureSheet(sheetName) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
sheet.appendRow(['Timestamp', 'Name', 'Email', 'Message', 'Source URL']);
sheet.setFrozenRows(1);
sheet.getRange('D:D').setWrap(true);
}
}
Then append the submission:
function appendRow(sheetName, row) {
SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(sheetName)
.appendRow(row);
}
Email notifications without SMTP
Apps Script includes MailApp.sendEmail(), so the script can notify the site owner without SendGrid, Mailgun, or SMTP setup.
MailApp.sendEmail({
to: cfg.recipientEmail,
subject: 'New Contact Form Submission',
body: `${name} <${email}> sent:\n\n${message}`
});
That is enough for many client sites.
HTML integration
Your frontend only needs a normal submit handler:
const res = await fetch(SCRIPT_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.name.value,
email: form.email.value,
message: form.message.value,
_gotcha: form._gotcha.value
})
});
const json = await res.json();
Deploy the Apps Script as a web app, set access to "Anyone", copy the deployment URL, and paste it into your form JavaScript.
When this is a good fit
This pattern works well for:
- portfolio contact forms
- agency landing pages
- small business websites
- waitlists
- internal request forms
- low-volume lead capture
It is not the right tool for file uploads, payment forms, or very high-volume transactional workloads.
Source code
I published a complete MIT-licensed implementation here:
https://github.com/ttcd77/form-handler
It includes Google Sheets storage, email notifications, honeypot spam protection, keyword filtering, rate limiting, auto-replies, and an example HTML form.
The project also has a one-time paid version for teams that want setup UI and multi-form quality-of-life features, but the open-source script is fully usable on its own.
Disclosure: this article was drafted with AI assistance and manually checked before posting.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.