Creating a contact form for a static website with AWS API Gateway and Lambda

Tutorial

April 25, 2020

If your website is just a bunch of plain .html and .css files (without a library like React which still counts as static) just like mine, but you still want a fancy contact form, then keep on reading.

In this blog post I'll be showing you how to make a contact form just like the one on my contact page using (mostly) vanilla javascript, some fun AWS services like API Gateway, Lambda, and SES, and even reCaptcha to add an extra layer of security.

Here's what we'll be building. Basically, upon form submission, the user's browser will call an API which will trigger a Lambda function in charge of getting the contact info, building the message, and sending it to our email.

Contact form CloudCraft diagram

The HTML form

We want to start with a very basic HTML form: a couple of input fields for the email and the message, and the submit button. You don't even have to use a <form> tag (but accessibility wise, you should), so I'll go for a <div>.


    <div>
        <input type="email" id="emailInput" placeholder="Enter email">
        <textarea id="messageInput" placeholder="message here"></textarea>
        <button id="submit" type="submit">Submit</button>
    </div>
        

Then, we want to do something when the submit button is clicked, and that is getting the values from the input fields and eventually send them somewhere. That somewhere will be a serverless backend built on top of AWS API Gateway and Lambda.

This is where the not so vanilla javascript comes into play; I'll be using axios to make a request (aka sending the data) to our still-to-be-created backend endpoint. To make it easier for us we can just import axios from their CDN with a script tag at the bottom of the page, and in another script tag handle the actual submission.

Axios is a promise-based HTTP client for the browser, if you are not familiar with it check out its documentation. You can still use vanilla javascript to make the requests.

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        const submit = document.querySelector('#submit')
        const email = document.querySelector('#emailInput')
        const message = document.querySelector('#messageInput')

        submit.onclick = () => {
            axios.post('my-still-to-be-created-endpoint', {
                email: email.value,
                message: message.value
            },
            {
                headers: {
                    ContentType: 'application/json'
                }
            })
            .then(res => {
                // Do something if call succeeds
            })
            .catch(err => {
                // Do something if call fails
            })
        }
    </script>
        

In the code snippet above we can see that we will be sending 2 parameters, one called 'email' and another one called 'message', each one with their respective value from the corresponding input. This is important for the next step.

The Lambda function

First, let's create the actual Lambda function; name it, select a runtime (I'll be using Python) and since we plan on using the Simple Email Service (SES), make sure you attach a role to your function with access to SES.

Creating the Lambda function

A role can be created in the IAM service of AWS. The AmazonSESFullAccess policy will work just fine.

Now, lets write the actual Lambda function:


    import json
    import boto3

    client = boto3.client("ses")

    def lambda_handler(event, context):

        contact_info = json.loads(event["body"])
        email_body = contact_info["email"] + "\n" + contact_info["message"]

        client.send_email(
            Source="your@email.com",
            Destination={
                "ToAddresses":["your@email.com"]
            },
            Message={
            "Subject": {
                "Data": "Contact form",
                "Charset": "UTF-8"
            },
            "Body": {
                "Text": {
                    "Data": email_body,
                    "Charset": "UTF-8"
                }
            }
            }
        )

        return {
            "isBase64Encoded": False,
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps("Sent!")
        }
        
Okay, let's break down what is happening:

Simple Email Service (SES)

In order to use SES we need to set it up; it is pretty straight forward.

Because you are (probably) going to be in sandbox mode (more on that here), we need to verify all the email addresses from which we wan to send and recieve emails.

Sandbox mode will work fine for now as an experiment, but for a production website with actual traffic, you should request production access.

From your AWS Console just go to SES > Email Addresses > Verify a New Email Address. After you select the email to verify, you'll recieve a message with a link to complete the process.

Setting up SES

API Gateway

It is time to create the actual endpoint where we will send the contact information. We are going to use the relatively new HTTP API provided by API Gateway as it is cheaper and a little bit more simpler and straight-forward, excellent for a small task just like this one.

In your AWS Web Console, first go to API Gateway > Create API > Build HTTP API. We'll start the basic configuration by adding a Lambda integration, selecting our region, and choosing our previously created Lambda function.

API Gateway step 1

When we get to the route configuration choose POST as the method, set a resource path like /contact (although it could be anything you want), and confirm the Lambda function mapped to that resource path.

API Gateway step 2

Accept the defaults on staging, review it, and (almost) done! We still need to configure CORS, otherwise we won't be able to call the API from our website.

Let's go to the Develop tab > CORS, and set Access-Control-Allow-Origin to our website (this allows calling the API from our domain), set Access-Control-Allow-Methods to POST (since it is the only method we specified), and Access-Control-Allow-Headers set to * to allow all headers.

API Gateway step 3

By clicking 'Save' all our changes will be deployed automatically because we chose the default stage earlier. That's it, we are done!

reCaptcha (optional)

To avoid (although maybe not entirely) bots filling and submitting our forms we can implement reCaptcha.

First, signup for the service, set the website you are going to use, and get your public and secret keys. Although the offical website provides docs on how to implement it, they can be confusing if it is your first time (it was for me). So here's what we need to do:

A very popular way to make requests in Python is using the requests library, however, it is not bundled by default in the Lamba environment (more on that here). We can of course import it ourselves, but that is outside of this tutorial's scope, so we'll use the urllib module instead which is supported, but a little bit less user friendly in my opinion.

Our front end will look like this:


    <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY_HERE"></script>
    <script>
        // querySelector...
        grecaptcha.ready(function() {
            grecaptcha.execute('SITE_KEY', {action: 'contact'}).then(function(token) {
                axios.post('my_lambda_endpoint/contact', {
                    email: email.value,
                    message: message.value,
                    token: token
                }, 
                // Rest of the function... 
    </script>
        

And now for our Lambda:


    from urllib.request import *
    from urllib import parse

    def lambda_handler(event, context):
        recaptcha_token = contact_info["token"]
        data = {
            "secret": "YOUR SECRET KEY", # Use an environment variable
            "response": recaptcha_token
        }
        data = parse.urlencode(data).encode()
        req = Request(url="https://www.google.com/recaptcha/api/siteverify", data=data, headers={}, method="POST")
        with urlopen(req) as res:
            body = json.loads(res.read().decode())
            
        if body["score"] > 0.5:
            # SES send email...
        

Next steps

To further improve this you can add exceptions on the Lambda function returning different things depending on the case, and based on this reponses display different things on the front end, like a success banner for example.

That's it, we are done. I hope I covered all you needed to make it work. If you are still having issues feel free to send me an email at info@amedpal.com.