L o a d i n g
How I Built My Contact Form Without a Backend Case Study

How I Built My Contact Form Without a Backend

My portfolio is a completely static site. No database, no login system, no dynamic anything — just HTML, CSS, and JS doing their job quietly. Everything about it worked exactly the way I wanted. Except one tiny, extremely obvious thing: the contact form.

You know, the thing whose entire job is to let a human being send me a message. Turns out that's the one feature a static site can't just... do. HTML can build you a beautiful form. It cannot, under any circumstances, send an email. It will just sit there, looking pretty, collecting nothing, going nowhere. A form with nowhere to submit to is basically a very polite piece of decoration.

Why not just use PHP?

The obvious old-school answer is "just throw a PHP script at it." And sure, that works. But my whole site had zero backend by design — no database, no login, no dynamic content anywhere. Bringing PHP back into the picture just to handle one form felt like buying a whole delivery truck because you need to send one letter. It also meant a PHP-capable host again, which is the exact thing I'd already escaped when I moved to Cloudflare in the first place (long story, that's a different post).

Static sites deploy faster, load faster, and have a much smaller attack surface since there's genuinely nothing for anyone to hack — no server-side code running, no database to leak. I wasn't about to break all of that for a contact form.

The actual problem

So here's what I actually needed: something to accept whatever a visitor types, check that it's not garbage, make sure it's not a bot, and then get it into my inbox. Four small asks. Somehow still a whole afternoon of research.

Shopping around

I looked at the usual suspects. Formspree looked simple enough — point your form's action at their endpoint and you're done — but the free tier limits felt tight, and I didn't love handing my form's plumbing to a third party I had zero control over. EmailJS was interesting too, sends straight from the browser, no backend needed, but exposing email logic client-side made me a little uneasy about abuse. Netlify Forms wasn't even on the table since I wasn't hosting on Netlify. Firebase came up in a few searches, but bringing in an entire Google backend-as-a-service for one form felt like the delivery-truck problem all over again, just with a different logo on it.

None of them felt like they actually belonged with the rest of my stack. I wanted something that lived where my site already lived.

Then I remembered I was already on Cloudflare

And that's when it clicked — I didn't need a new service at all. I was already hosting the whole site on Cloudflare Pages. Cloudflare Workers could run the backend logic for me, serverless, no actual server to manage or pay for. Pair that with D1 (Cloudflare's own SQL database) to log every submission, and suddenly I had a full mini-backend that lived in the exact same ecosystem as everything else — same dashboard, same GitHub integration, same account. No new signup, no new API keys scattered across five services, no new thing to forget I'm paying for.

How it actually works

Stripped down, the flow looks like this:

  • Visitor fills out the contact form and hits submit
  • The browser sends that data to a Cloudflare Worker
  • The Worker validates it — checks required fields are actually filled, nothing looks malicious
  • Security checks run (more on that in a second)
  • If everything checks out, the Worker sends me an email and logs the message in D1
  • The visitor gets a "message sent" confirmation, and goes about their day never knowing any of this just happened

From the visitor's side, it looks exactly like a normal form. Underneath, it's a whole little serverless pipeline doing its job in milliseconds.

Security, because a form is basically a spam magnet with a submit button

Here's the thing nobody tells beginners: even a tiny personal contact form gets hit by bots. Not "might," gets. The internet is full of scripts that find any form on any site and start throwing junk at it, hoping something sticks. So I wasn't about to skip protection just because I'm not exactly running a Fortune 500 company from my bedroom.

I added reCAPTCHA to stop the obvious bots, server-side validation so the Worker never trusts anything blindly, proper CORS so random other websites can't quietly submit through my endpoint, and input sanitization so nobody gets cute and tries to inject something nasty into a text field that was only ever supposed to say "hi, loved your site."

What I actually gained from this

  • No PHP, no server to patch or worry about
  • No database to manage beyond D1, which Cloudflare handles for me
  • Still completely free, on the same plan I was already using
  • Fast — the Worker runs on Cloudflare's edge, close to wherever the visitor actually is
  • Deploys automatically through the same GitHub integration as the rest of the site
  • One less thing to maintain separately, because it's not separate at all

It wasn't all smooth, obviously

Nothing ever is. CORS errors ate a solid chunk of an evening — that classic moment where the request works perfectly from a REST client but the browser just refuses to cooperate, no clear reason given, good luck. Learning how Workers actually think, request/response objects, environment bindings, none of it clicked instantly. Testing deployments meant a lot of "why is this working locally but not live" moments that turned out to be one missing environment variable each time. Small, dumb problems, solved one at a time, the way most real problems get solved.

What I actually learned

This ended up teaching me a lot more than "how to send an email from a form." It was my first real hands-on look at serverless architecture, working with APIs properly, understanding a deployment pipeline end to end, and generally how modern web apps are stitched together without a traditional always-on server sitting in the back doing everything.

A contact form is supposed to be the boring part of a website. Mine ended up teaching me more than half the "impressive" features I've built.

Looking back, this little form became a much bigger learning experience than I expected going in. If you're building something static and hit a wall the moment you need "just one dynamic thing," don't automatically reach for a full backend. Sometimes the right answer is smaller and weirder than that, and it's worth poking around before you commit to dragging a whole server into your life for it.

And hey, since you made it this far — go ahead and actually test the thing. Head over to my contact page and send me a message. Yes, this is a shameless plug. Yes, it is also the exact form this entire post is about. Full circle.

🎵 My Vibe Akhar — Amrinder Gill