DEV Community

Cover image for 40 hours I wasted before I built my own form backend
Łukasz Blania
Łukasz Blania

Posted on

40 hours I wasted before I built my own form backend

Last Friday night I opened my GitHub and ran a search across every personal repo. The query: anything mentioning "form", "submission", "contact", or "POST /". Forty seven results came back. Most of them were variations of the same file.

I sat there counting. Twelve different projects. Five different stacks (PHP, Express, Hapi, FastAPI, Next API routes). Each one had a handler that did roughly the same things. Receive POST. Validate. Check honeypot. Save somewhere. Send email.

I tried to estimate how long each one took. Three hours minimum for the simple ones. Six or seven for the ones that needed Slack notifications or file uploads. Add up the dozen, factor in the bugs I caught later, count the rate limit code I copy pasted from Stack Overflow each time.

The number came out to forty hours. A full work week, spent solving the same problem over and over, across six years.

That night I started building FormTo, the tool I should have written in 2019. This post is the story of why it took me so long, and the things I wish someone had said to me back then.

TL;DR

If you have built a contact form for more than one project, you have probably already lost more hours than you think. The handler feels small every time. The handler is not the problem. The hidden tax around the handler (spam, deliverability, notifications, exports, edge cases) is what eats your weekends. Building once and reusing is cheaper, even if you have to pay nine bucks a month or self host something.

The first handler (2018)

A friend asked me to build a small site for his consulting practice. Plain HTML, a couple of pages, a contact form. I quoted him five hundred euros. We agreed on a deadline.

The HTML and CSS took two evenings. The contact form took three. I wrote a PHP script that mailed me whenever someone submitted. It worked. He paid me. I went on with my life.

Two weeks later he called. The form was getting spam. Like a lot of spam. I added a honeypot field, redeployed, and went back to bed. Three weeks after that he called again. Email delivery from his shared hosting was failing for half the recipients. I migrated him to Mailgun, added DKIM and SPF records to his DNS, billed him for the extra work, and felt smart.

I had no idea that this small contact form would become a pattern I would repeat for every freelance project for the next six years.

The exact same thing, twelve times

In 2019 I wrote an Express handler for a client whose stack was Node. Same five steps, slightly different syntax. Mailgun for email again.

In 2020 I wrote a Hapi handler for a startup that hired me for two months. Their dev lead insisted on Hapi, which I had never used. I learned just enough Hapi to write the form handler, then forgot all of it.

In 2021 I wrote a FastAPI handler for my own SaaS, the one before this one. I added a small queue because submissions were spiky.

In 2022 I wrote two more Next API route handlers. By this point I had a private gist with my "starter snippet" that I copy pasted in. The snippet was 180 lines.

In 2023 and 2024 I wrote four more. The snippet had grown to 240 lines. I had added blocklist support, file upload validation, and a hacky retry loop for failed email sends.

Each time, the first hour of work was "ok, what was that thing I did last time". Each time, the next hour was "wait, my snippet does not handle this new edge case". Each time, the third hour was a fresh round of testing against Postman.

Three hours, twelve times. Plus rework, plus the spam incident in 2020, plus the deliverability mess in 2022, plus the time I shipped a regex for email validation that rejected anything with a plus sign in the address.

Forty hours.

The night I broke

Last summer I deployed a small admin form for a client at 11 PM on a Friday. I was tired. I skipped the honeypot field "because the form is behind auth anyway". My wife came in to ask when I would be in bed. I said twenty minutes.

At 2 AM I was still working. The form was open behind auth, but auth let unauthenticated POSTs through because of a misconfigured middleware. A bot found the endpoint within an hour of deploy and submitted four thousand fake leads. Most of them triggered the email notification to my client. His inbox was destroyed.

Worse: one of the four thousand was a real lead. A real customer had used the same form to ask about a contract. The real email was buried in the spam, my client missed it, the deal went somewhere else.

I patched the middleware, added a honeypot at 3 AM, refunded my client for the missed deal, and went to bed angry. Not at my client. At myself, for shipping the same broken pattern for the seventh year in a row.

Two weeks later I started counting the hours. That was when I decided to build the thing.

What I actually learned

I built FormTo over four months of evenings. The act of writing it as a product, instead of a snippet, forced me to think about the things I had been sweeping under the rug for six years.

Honeypots beat CAPTCHA most of the time

Every project I shipped had a CAPTCHA on the form by default. Most of those forms did not need one. A hidden field with a bait name (something like website, url, or phone_number) catches almost every bot, costs zero user experience, and works without JavaScript.

I now check for nineteen common bait names automatically. CAPTCHA is the last resort, not the first move. Most of the spam I used to get was solved by removing CAPTCHA and adding honeypots.

Email delivery is its own job

I spent more time over the years debugging email delivery than I did writing form handlers. Shared hosting SMTP. DKIM set up but not signing. SPF too loose. Hosts whose IPs landed on a blocklist between Tuesday and Thursday.

The pattern I settled on: a default Resend integration that works out of the box, plus the option to plug in your own SMTP credentials so emails ship from your own domain. Your domain gets the reputation. Your inbox gets the replies. The host does not see the email content.

Notifications matter more than the dashboard

For six years I assumed people would log in to a dashboard to see new submissions. They do not. They check email for ten minutes, then they tune it out. The signal that actually gets read is Slack, Discord, or Telegram. Especially Telegram, which I underestimated for years.

Per form notification toggles, with the credentials saved at the user level. One Telegram bot for all your forms. The dashboard exists, but most people barely open it after the first week.

File uploads are a tax

Every form tool I have ever used either pretends file uploads do not exist or adds them as an enterprise feature. I added them as part of the paid plans. The implementation is small: multipart parsing, MIME validation, a size limit per tier. The reason I had skipped this for six years across freelance projects was not that it was hard. It was that I always told myself "the client will not need this" and then they always did.

Self hosting is an option, not a headline

I open sourced the backend under AGPL 3.0. Docker Compose, Caddy, PostgreSQL. No telemetry. People who want to run it themselves can. The vast majority do not want to, and that is fine. The SaaS pays for the maintenance, the self host option keeps me honest. Both versions read from the same codebase.

When the snippet is still the right answer

This post would be useless if I did not say where the trade goes the other way.

Use a form backend (mine or anyone else's) when the form is a contact form, a feedback form, a waitlist signup, or any case where the submission lives on its own and just needs to land somewhere visible.

Write your own handler when the form is part of a larger workflow with conditional logic per step, when the submission needs to write across half a dozen tables, or when you are already running a backend with fifty endpoints and the form is just one more.

I still write custom handlers. I just do not write them for contact forms anymore.

The math, redone

This morning I ran the same GitHub search I ran on that Friday night. Same query. Same repos. I filtered out anything from before FormTo shipped.

Six projects shipped after I started using my own tool. Total time spent on form handling across those six projects: about forty minutes. Most of that was pasting the action URL and writing the autoresponder template.

Forty hours over six years, replaced with forty minutes over one year. That ratio is the entire reason this tool exists.

How many hours have you spent writing the same form handler? I am genuinely curious. Mine was forty. I have a feeling yours is higher than you want to admit.

Top comments (0)