Serverless contact forms with AWS Lambda

Contact forms. They are all around us. Almost every website and mobile app has one. And in a lot of cases they are the only part of the website which needs some backend code to work. The beauty of having a static HTML website is the simplicity of hosting and maintaining it.

But when the contact form enters the game things get complicated quickly. Suddenly you need to worry about things like — How will I process the request from the contact form? How will I send out the email? Which backend technology should I use just for that simple task? Where will I run my backend and later on how will I maintain my backend and make sure that it is running? All those questions just for the purpose of supporting your contact form.

I am proposing a solution where you would use Amazon Web Services (AWS) to make your life simple and handle your contact form in a very efficient and cheap way. The basis for the solution is AWS Lambda — the serverless compute engine from Amazon. What is great about AWS Lambda is that you pay only when your code is running, which in this case is only when somebody submits your contact form, and that will cost you a fraction of a dollar. There are no ongoing server prices and no ongoing worries whether your server is working.

We will use the following AWS products:

  • API Gateway — it will be used to receive a POST request from our static website
  • Lambda — it will be used to process the request received by API Gateway and send the email. Our Lambda function in this case will be ran on Node.
  • SES — Amazon’s Simple Email Service will be used by Lambda to actually send the email
Diagram of how data will flow from our contact form, to AWS lambda where email is being sent and then back to our contact form with a response.

I have separated this guide in logical steps as you would encounter them in the real world. And at the end of each step we will test whether the thing is working at that point in time.

Section 1: setting up the API Endpoint and a dummy AWS Lambda function

The goal of this step is to get an Endpoint URL where we will send our contact form’s request. We’ll also create a dummy Lambda function which will transform the request and just respond back with a static response for testing purposes.

Set up AWS Lambda

  • Under your AWS Console search for Lambda
  • When you are inside the Lambda console click on the ‘Create Function’

A wizard for the creation of the function will open and it will have 4 steps.

Step 1: Select blueprint

Search for ‘hello-world’ and ‘node’ and select the hello-world, A starter AWS Lambda function, nodejs blueprint.

Step 2: Configure triggers

Skip the Add trigger step for now by clicking the ‘Next’ button.

Step 3: Configure function

Configure function step has 5 sections. For this example we’ll only use the first three sections: Basic information, Lambda function code and Lambda function handler and role. We’ll ignore Tags and Advanced sections. Ok so let’s go through each section.

Basic information

Here we’ll just give our function a name, for this example let’s call it processContactFormRequest. You can change the description if you like and leave the default Runtime set to Node.js.

Lambda function code

You will see a code sample which came with the blueprint. We’ll replace it with the following code for now:

exports.handler = (event, context, callback) => {
console.log(‘Received event:’, event);
var response = {
"isBase64Encoded": false,
"headers": { 'Content-Type': 'application/json'},
"statusCode": 200,
"body": "{\"result\": \"Success.\"}"
};
callback(null, response);
};

This code does nothing except it returns a response in a format which Lambda function requires when we do a Proxy integration. The response format is explained in the official docs.

Lambda function handler and role

For the Handler leave the default index.handler.

For the Role select the Create a custom role. It will open up a new tab with AWS IAM page with a form for Role creation. The screen should look something like this.

What we need to do here is create a lambda_basic_execution role with permissions to use Cloudwatch Logs (for writing to logs) and SES (for sending emails). The permissions we set here will apply to our Lambda function.

Click on Edit under the Policy document and place the following JSON there.

{
"Version": "2012–10–17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ses:SendEmail"
],
"Resource": [
"*"
]}]}

Save and go back to Lambda function wizard. As mentioned earlier we can skip the Tags and Advanced sections. So by having filled out the Basic information, Lambda function code and Lambda function handler and role our Lambda function is ready. Proceed by clicking the Create function button.

Now that we have our Lambda function in place let’s connect it with API Gateway.

Set up AWS API Gateway

  • Under AWS Console search for API Gateway
  • When you are in the API Gateway console click on the Create API button
  • On the Create API screen select the New API and give the API a name. For this example I will call our API staticWebsiteDemoApi
  • Then on the API page, click the Actions dropdown and select Create Method
  • Select the following options: Integration Type: Lambda Function; Use Lambda Proxy Integration — Check; Lambda region — choose the same region where you have placed your Lambda function from previous step; Lambda function: place the name of the function we used in the previous step, in this case processContactFormRequest

Remember that before you can use the API you always need to go to Actions > Deploy API. Also when you do some changes on the API you always need to Deploy it to see those changes in effect.

When you deploy you’ll need to create a Stage, and once you do you’ll see the Invoke URL for the API. This URL will be used by our Contact Form to send the POST Request.

Let’s test out what we have done so far. Open Postman (a tool for testing out requests), set the URL, set the request to POST and click the Send button.

In the response window we should see the body of the response we have set in our Lambda function.

{"result": "Success."}

Great! We have a working API endpoint where we can send the request, it gets processed and we see the response.

Section 2: create a simple contact form with jQuery

In this step we’ll create a simple contact form and jQuery code for sending the form data to our API endpoint.

Here is a basic html page which will show a contact form (a very ugly one) and supporting jQuery code to send the form data to our API endpoint.

<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {

$("#submit").click(function(e) {
e.preventDefault();

var name = $("#name").val(),
email = $("#email").val(),
message = $("#message").val();

$.ajax({
type: "POST",
url: 'URL_ENDPOINT_FROM_LAST_STEP',
contentType: 'application/json',
data: JSON.stringify({
'name': name,
'email': email,
'message': message
}),
success: function(res){
$('#form-response').text('Email was sent.');
},
error: function(){
$('#form-response').text('Error.');
}
});

})

});
</script>
</head>
<body>
<form>
<label for="name">Name</label>
<input id="name">
<label for="email">Email</label>
<input id="email">
<label for="message">Message</label>
<textarea id="message"></textarea>
<button id="submit">Submit</button>
</form>
<div id="form-response"></div>
</body>
</html>

Now when we submit the form we will run into the CORS problem. If you open up the Developer console in Chrome you will see something like this.

Browser is rejecting our request because of browsers security feature which prevents the request to be made to a different domain.

Browsers have a security feature which prevents the HTTP requests to be made to the different domain. It allows only HTTP requests to be made to the same domain where the website is residing. And in this case our API is on a different domain so the browser is blocking the request. The solution to this problem is to set up CORS.

Let’s fix it.

Go to the AWS console and find the previously created API Gateway endpoint which we called staticWebsiteDemoApi. Under resources select the POST method we created and under Actions select Enable CORS. Leave the default options and click on Enable CORS and replace the existing CORS headers.

After that we also need to update our Lambda functions response to include the Access-Control-Allow-Origin header.

We will add this to our Lambda function: ‘Access-Control-Allow-Origin’: ‘http://example.com’ (change the domain here to match your hosting domain, or set it to your localhost domain if you are doing this locally), so the new Lambda code looks like:

exports.handler = (event, context, callback) => {
console.log(‘Received event:’, event);
var response = {
"isBase64Encoded": false,
"headers": { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://example.com'},
"statusCode": 200,
"body": "{\"result\": \"Success.\"}"
};
callback(null, response);
};

Save the Lambda function and voila, we can submit our form and get the success response!

Section 3: update the Lambda function to do the actual email sending

For the last step we just need to finish our Lambda function code to include the email sending. For this example let’s keep it simple.

var AWS = require('aws-sdk')
var ses = new AWS.SES()

var RECEIVERS = ['receiver@example.com'];
var SENDER = 'sender@example.com'; // make sure that the sender email is properly set up in your Amazon SES

exports.handler = (event, context, callback) => {
console.log('Received event:', event);
sendEmail(event, function (err, data) {
var response = {
"isBase64Encoded": false,
"headers": { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://example.com' },
"statusCode": 200,
"body": "{\"result\": \"Success.\"}"
};
callback(err, response);
});
};

function sendEmail (event, done) {
var data = JSON.parse(event.body);

var params = {
Destination: {
ToAddresses: RECEIVERS
},
Message: {
Body: {
Text: {
Data: 'Name: ' + data.name + '\nEmail: ' + data.email + '\nMessage: ' + data.message,
Charset: 'UTF-8'
}
},
Subject: {
Data: 'Contact Form inquiry: ' + data.name,
Charset: 'UTF-8'
}
},
Source: SENDER
}
ses.sendEmail(params, done);
}

Now we can submit our contact form and it will be processed by the AWS Lambda function. This example is missing form validation and better data and error handling, but you can easily tailor this to fit your needs.

With this architecture stack you can have your static website without the need to worry about the backend, AND you pay for this only when somebody sends you the inquiry on your contact form. This is especially valuable to somebody who receives a low volume of contact form requests.

If you run in to some problems along the way feel free to leave a response here, or DM me on Twitter Marko Francekovic.