Handling form submissions is (still) a time-consuming and disappointing task. Sometimes all you want is to create a form that triggers an email with the provided details. The sad truth is, there is no such solution available for free.
But there is a solution that comes with nearly zero cost and effort: Utilizing AWS Lambda and AWS Simple Email Service (SES).
If you never worked with AWS, no worries, this requires absolutely no in-depth knowledge. All you need to know, is how to navigate AWS. However, I recommend you read about the Well-Architected Framework and to create a budget alert when you're starting to use AWS to avoid costly mistakes.
Components
Even though it might seem trivial, I want to lose a few words to the components in use. Feel free to skip this section if you're already familiar with the AWS API Gateway, AWS Lambda, and AWS SES Services.
API Gateway
The API Gateway is a fully managed service that allows exposing business logic via API endpoints. The API Gateway supports containerized, serverless workloads as well as web applications. It is the entry point to your application logic.
We utilize its feature to invoke Lambdas via a public endpoint in our case.
Lambda
Lambda is based on the serverless concept. Instead of ever-running EC2 instances (the AWS solution for virtual machines), lambdas only consume resources when they are invoked. The actual costs are calculated in Gigabyteseconds (GBS). This unit is calculated by multiplying the required main memory times the execution time. For example, if you have a memory consumption of 80MB and your Lambda takes 560ms to compute, this equals ~0.044GBS
1GBS is currently billed at $0.00001667 which means your request effectively costs $0.0000007293125 or in other words, you can make about 1.37 million requests until you have to pay a single dollar.
The savings in resources are hilariously high. While virtual machines usually cost at least a few cents an hour at any given cloud provider; you can make thousands of serverless requests until you have to pay the same.
Simple Email Service (SES)
The AWS SES is a service to send and receive emails. It supports various nice features such as DKIM. This will almost certainly ensure your mails won't get tagged as spam as long as your content does not behave like phishing.
To use SES in this scenario, you will have to confirm the email or domain you're going to use to send mails. The SES wizard is clear and concise about the instructions; you should not run into any issues here.
Why use AWS
When it comes to architecture, in 9 of 10 cases, the definitive answer to your question is: „it depends“. So maybe let's take one step back and talk about why this solution might fit your use case.
The two factors that led to my decision were Availability and Costs. Neither is a solution with low availability nor high costs tolerable for submitting mere forms.
Availability
The availability of this solution is at least 99,9%. Which equals about 8.7 hours of downtime per year. The AWS API Gateway and the AWS Lambda Service have an availability of at least 99,95%, according to their service level agreements (SLA).
mathAvailability(API Gateway, Lambda)=0.9995 * 0.9995=~0.999 = 99.9\%
This is enterprise level availability and most likely much higher than the availability of your self-hosted backend. Also, if necessary, it would be pretty easy to increase the availability by utilizing availability zones, for instance.
Costs
The costs are staggeringly low. All of this easily fits in the free-tier offering of AWS and thereby your costs for the first 12 months would be $0.
After that, your costs are that low; they won't even sum up to a total of $1 per year. But let's calculate that.
Let's assume:
- your website is frequently visited, and 50 people submit a form each day
- the lambda takes 75MB of main memory and 80ms to complete (my average)
That would be 18250 submissions (50*365) and ~107GBS (75/1024GB*0,08s*18250 submissions) per year.
- 18250 requests via the API Gateway are billed with $0.0219 ($1.20 per million)
- 107 GBS via Lambda are billed at $0.00178 ($0.00001667 per GBS)
- 18250 emails via SES are billed with $0 (the first 62 000 each month are free)
This sums up to a total of $0.02 per year. I don't think there is any other managed solution that can match these costs. But feel free to prove me wrong in the comments.
Lambda Function
First, we create a new lambda. Navigate to Lambda on the AWS Cloud web interface. Make sure your region is set correctly (at the top bar on the right side). Click on Create function, add your desired function name and select NodeJS as runtime, and click on Create function again (at the bottom).
After the creation succeeded, you should now be in the integrated code editor with some default code:
exports.handler = async event => {// TODO implementconst response = {statusCode: 200,body: JSON.stringify('Hello from Lambda!')}return response}
As you can see here, a lambda function is exported by exports.handler
and is
an asynchronous function with a single argument event
.
Now let's paste in our code to process the form submission:
const AWS = require('aws-sdk') // Import the aws sdkconst ses = new AWS.SES({ region: 'eu-central-1' }) // Create an SES client bound to eu-central-1const RECEIVER = 'info@code-specialist.com' // The recipientconst SENDER = 'no-reply@code-specialist.com' // The sender (Must be configured with SES)const response = {// The response we're going to send to the requesterstatusCode: 200,body: JSON.stringify('Success')}exports.handler = async function (event) {// The actual lambdaconsole.log('Received event:', event) // To log the event, make sure not to violate applicable data protection directives!const message = await decodeAndTransformContent(event.body) // Decode and transform the form submissionawait sendEmail(message) // Send the emailreturn response // Return the response}async function decodeAndTransformContent (eventBody) {const buffer = Buffer.from(eventBody, 'base64') // Create a bufferconst content = buffer.toString('ascii') // Decode the base64 encoded formconst tuples = content.split('&') // "a=b&c=d" => ["a=b", "c=d"]const kv_pairs = tuples.map(pairs => pairs.split('=')) // ["a=b", "c=d"] => [["a", "b"], ["c", "d"]]const formatted = kv_pairs.map(pairs => `<b>${pairs[0]}</b>: ${pairs[1]}`) // [["a", "b"], ["c", "d"]] => ["<b>a</b>: c", ...]return formatted.join('<br/>') // ["<b>a</b>: c", ...] => "<b>a</b>: b <br/>..."}async function sendEmail (message) {const params = {Destination: {ToAddresses: [RECEIVER] // A list of recipients},Message: {Body: {Html: {Data: message // The actual content}},Subject: {Data: 'Contact Form Submission' // Your subject}},Source: SENDER // The sender of the mail (Must be configured with SES)}return ses.sendEmail(params).promise() // Return the sendEmail promise}
All of this code should be self-explanatory. If not, the comments should clarify what's happening. The serverless function will take any given form event and create a HTML message body for all the key-value pairs provided.
Configure the Policy
When you created the lambda, a basic policy has been created automatically to execute the lambda. We now have to extend this policy, so it also allows us to send emails via SES.
Navigate to the IAM service and then to Policies. You should
find something named similar to
AWSLambdaBasicExecutionRole-5749fdbf-a74e-49a6-a18e-e8dc2d5e7c98
. Click on it
and then press Edit policy. By default, this will open a visual editor.
Click on the JSON tab instead.
You should now see the active policy statements in a JSON format:
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": "logs:CreateLogGroup","Resource": "arn:aws:logs:eu-central-1:1234567891011:*"},{"Effect": "Allow","Action": ["logs:CreateLogStream","logs:PutLogEvents"],"Resource": ["arn:aws:logs:eu-central-1:1234567891011:log-group:/aws/lambda/formFunction:*"]}]}
Add another statement:
...{"Effect": "Allow","Action": ["ses:SendEmail","ses:SendRawEmail"],"Resource": "*"}...
Finally, click on Review policy and afterward on Save changes.
Create an API Gateway
Last but not least, we need to create an API Gateway to expose the Lambda we
defined via a POST
action. To do so, navigate to the API Gateway service.
Click on Create API and select HTTP API. You can now click on Add
integration and select your Lambda. Next, choose your API's name and click on
Next. You should now see an UI to configure your routes Select the method
POST
for the lambda and choose a resource path for it. The resource path will
be the relative address of your endpoint. Leave everything else as it is and
navigate via Next until you can click on Create.
You should now see an Invoke URL like
https://code-specialistxyz.execute-api.eu-central-1.amazonaws.com
, which is the
base URL of your API. To call your Lambda you can now execute a post to
https://code-specialistxyz.execute-api.eu-central-1.amazonaws.com/contactForm
assuming your resource path is contactForm
.
Testing the function
To test your endpoint, you could use this minimal HTML5 snippet:
<!DOCTYPE html><html><head><meta charset="utf-8" /><title>Test Form</title></head><body><!-- Make sure to use your endpoints address below --><formaction="https://code-specialistxyz.execute-api.eu-central-1.amazonaws.com/contactForm"method="post"><span>Name</span><input name="name" /><br /><span>Email</span><input name="email" /><br /><span>Message</span><input name="message" /><br /><button type="submit">Submit</button></form></body></html>
And that's basically it. You're ready to go! 🚀
If anything didn't work as expected or you're in need of an explanation somewhere, don't hesitate to leave us a comment 😊