Simple Backends: Four ways to implement a “Contact Us” form on a static website
It’s a fairly common problem: you’ve designed a sleek, fast, static website — maybe a homepage for a new product, a portfolio of your work, or documentation for an open source library — and now you want to add a more interactive feature, like a “Contact” form. This generally requires some backend logic, multiplying your hosting costs, not just monetarily, but in terms of complexity and implementation time as well.
Here’s why: with a purely static site, everything is done in the frontend. The user just has to download a handful of files that run in her browser, and these files can be hosted on GitHub Pages or Amazon S3 for free (or close-to-free). But when you need code to run on the backend, a server or third-party service needs to be provisioned to listen for whatever events you’re interested in, such as the submission of a “Contact” form.
Below I’ll illustrate a few possible solutions to the need for small amounts of backend logic, using a “Contact Us” form as an example. Each comes with pros and cons: some are easier to implement, but lack customization; others are harder to implement but can be customized ad infinitum. Some rely on third-party solutions, while others leave you with full ownership — and full responsibility for uptime and maintenance.
1. Link with mailto:email@example.com
The simplest solution for dealing with backend logic is to avoid it entirely. Rather than having an interactive “Contact” form, we could simply add a
mailto: link — this requires no backend logic at all! When users click the link, their default e-mail client will open up a new draft, pre-populated with your e-mail address in the
Nice and easy! We can even pre-populate the
This is a great start, and the fact that it doesn’t require any backend logic is great. But there are a few drawbacks:
- Your e-mail address is now publicly available (get ready for spam!)
- An embedded form, where the user can type their message directly on your website, would look more professional and likely have a higher interaction rate
- We can’t require additional fields, such as a job title or company name
- You’re stuck with e-mail — if you want contact events to create a Trello card, a new item in your issue tracker, or a new lead in your CRM, you’re out of luck
To address these issues, we’ll need to start looking into backend solutions.
2. Run your own server
The de-facto solution to running backend logic is to write an HTTP server, e.g. in PHP, Ruby, or NodeJS. Here we’ll use NodeJS with Express.
This is simple enough: we create an HTTP server that listens for calls to
POST /contact. By placing this server behind a domain, e.g.
api.company.com, we can create an HTML form that will trigger it:
The above example will just log the message to the console whenever the form is submitted, but we can fill out the
POST /contact route with whatever logic we want. For example, if we want to get a new message in our inbox when a user submits the contact form, we can use a Gmail client to send ourselves an e-mail (note that the Gmail account doesn’t have to be the same as the destination address — you can create a separate account to better isolate your credentials):
This is a major improvement over our
mailto: link — now our e-mail address is private, and users can send messages directly from our webpage rather than being kicked out to an e-mail client. But there are still some major drawbacks, mainly around complexity and maintenance:
- We need to provision a server to run our NodeJS code, e.g. an Amazon EC2 instance. This will cost at least a few dollars a month.
- Our server needs to run in the background constantly, even though it will just be sitting idly until someone hits the contact form. This is a huge waste of resources.
- Our server could crash or fail at any moment — we need to monitor our server, either by manually testing the contact form periodically, or by using more complex (and expensive) solutions like AWS deployment groups with ELB.
- We most likely want to serve this over HTTPS, so no one can eavesdrop on contact messages (and so Chrome doesn’t brand our page as “not secure”). Provisioning and maintaining SSL certs is costly and annoying.
Fortunately, in the modern age of cloud computing, there are services which abstract away the nitty-gritty implementation details of running backend code.
3. Functions as a Service
One of the most powerful paradigms in cloud computing is the notion of Functions as a Service (FaaS). You may have heard of FaaS in the context of serverless architectures or microservices. The basic idea is to abstract away all the pain of provisioning a server, keeping it running, etc — instead, just write some code (e.g. in NodeJS or Python) and tell the FaaS provider when it should run (e.g. on a schedule or in response to an HTTP request).
Here we’ll use the Serverless framework, which supports several different FaaS providers. We’ll select AWS Lambda as our FaaS provider of choice, but the code is similar for Azure, OpenWhisk, and Google Cloud Functions.
First we need to create our function in
This looks a lot like our Express route above, but this time our logic lives inside an AWS Lambda handler.
Next we use
serverless.yml to make sure this handler runs whenever the
POST /contact URL is called:
If your AWS credentials are set, you can run
serverless deploy to get this running on AWS Lambda with API Gateway — this will give you a URL to use in your HTML
<form> tag, e.g.
https://abcd1234.execute-api.us-east-1.amazonaws.com/prod/contact. Optionally, you can also register a unique domain using Route53 and set up a free SSL certificate using AWS Certificate Manager.
This is a much better solution than provisioning our own server. Before, we had a machine running constantly, mostly just sitting idly until someone touched the contact form. Now, API Gateway will listen for requests to
POST /contact, and will spin up a machine to run our code only when it’s called. So instead of tying up a server 24/7, we’re only using computational resources for maybe a few seconds per day; unless you’re getting over a million messages per month, this lambda will be easily covered by the free tier.
Best of all, our monitoring and maintenance burden has been drastically reduced. FaaS is meant to be stateless, so each time the
POST /contact URL is hit, we get a fresh environment (sort of). Events that would cause our Express server to crash permanently will fail gracefully when using FaaS.
4. Integration Platforms
Integration platforms like Zapier and DataFire offer a sort of variation on FaaS. They assume your backend logic will be heavily reliant on third-party services like Gmail, GitHub, Slack, or Trello, and make it simple to plug into those systems, often without writing any code.
For example, with DataFire, we can create an action that sends us an e-mail via Gmail by creating a new project and opening the action editor:
What’s great about integration platforms is that we don’t need to read up on the documentation for the services we want to use — the platform will guide us through each API call and suggest autocompletions:
Even better, integration platforms allow us to easily import and scope our credentials — instead of passing around our username and password like in the above examples, we can use the interface to generate a new access token:
Finally, just like in the Serverless example, we need to tell DataFire to run our action in response to the
POST /contact URL:
Clicking Deploy will create an endpoint at
https://contact-us.prod.with-datafire.io/contact — we can use this URL in our HTML
I hope this has been a helpful overview of some options available for running small amounts of backend logic. Of course, a “Contact” form is just one simple use case among many — anytime credentials need to be hidden from the client device, you want logic to run on a schedule, or you want to provide a programmatic API, you’ll need some kind of backend. FaaS providers and integration platforms can be immensely helpful here, especially if you want to avoid the headache of provisioning, monitoring, maintaining, and scaling your own servers.
Here are a few other cases where adding a touch of backend logic to an otherwise client-only app can make a huge difference in both user and developer experience:
- Sign-in with OAuth — if you want to deploy a static webpage that e.g. pulls data from a user’s GitHub account, you’ll (usually) need a backend to handle the OAuth handshake
- Content management — use a backend to pull data from a private Google Sheet, which can be updated live by non-technical teammates
- Sales lead generation — when a user performs certain actions in your app or webpage, use the backend to make a note or create a new lead in your CRM
We’d love to hear your ideas in the comments!