Creating a Contact Form with AWS Lambdas

Posted by Niky Morgan on March 23, 2018

I recently moved my web hosting to AWS. (Blog post here.) I knew from friends that hosting a static site on AWS is relatively cheap and easy, so I was sure I’d be able to make the switch. However, I didn’t take the time to consider that not all the elements of my site were static.

On my previous site I had a contact form which included a PHP script. JavaScript would take input from the form and send a post request with the form data to a PHP file. The PHP script validated the input and sent me email if it passed. Click here for a blog post on how I built that.

My previous hosting service supported PHP and would serve that script. If I wanted to do the same on AWS, I’d have to set up an EC2 instance to serve it. This was definitely an option, but it would require keeping a server running constantly just for that one script. Since EC2 charges accrue based on the time spent running, it isn’t an efficient use of computing resources or money to keep a server up just listening for a form submission. However, AWS also provides a way to run scripts without a server. With Lambda functions, you can write code which will be triggered by specific events. Lambdas only incur charges when the code is run, so I wouldn’t have to pay for all that server time waiting for something to happen. Instead I can set up an API endpoint which will run my Lambda function when it receives a request.

Using AWS services the overall structure of my contact form is now this:

  • JavaScript and HTML assets for my website are statically served from an S3 bucket
  • my contact form has a JavaScript function which sends a post request with form data to an API endpoint set up through API Gateway
  • the post request to my API endpoint triggers a Lambda function when it receives this data
  • the Lambda function sends me an email with the form data through SES (Simple Email Service)

Below is my HTML form. I assigned each input field an ID so that I could easily grab the data in those fields to submit in my JavaScript.

<form id='contact-form'>
  <label for='form-name'>Name</label>
  <input type='text' id='form-name' placeholder='Name' autocomplete='name' required>
  <label for='form-email'>Email</label>
  <input type='email' id='form-email' placeholder='Email' autocomplete='email' required>
  <label for='form-message'>Message</label>
  <textarea rows='5' id='form-message' placeholder='Message' autocomplete='message'  aria-invalid='false' required data-validation-required-message='Please enter a message.'></textarea>
  <button type='submit' id='form-submit'>Send</button>
</form>

I declared a handleSubmit function which listens for the form to be submitted. It grabs each form input field and packages the data in a stringified object to send to the Amazon endpoint which will trigger a Lambda. I didn’t include the handleSuccess and handleFailure functions referenced because they just reset the form and/or display messages about the status of the form submission. In my handleSubmit function I set a variable url which holds the url to which the post request should be made. This url won’t be created for a few steps yet, so I left a placeholder in the code below.

function handleSubmit(event) {
  event.preventDefault()
  var name = $('#form-name').val()
  var email = $('#form-email').val()
  var message = $('#form-message').val()

  if (validateEmail(email)) {
    var url = /* my amazon endpoint */
    var data = {
      name: name,
      email: email,
      message: message
    }
    $.post(url, JSON.stringify(data))
      .done(handleSuccess)
      .fail(handleFailure)
  }
}

function validateEmail(email) {
  var validator = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/

  if (!validator.test(email)) {
    // handles printing my error messages
  }
  return validator.test(email)
}

To set up the code which will send my email, I navigated to Lambda in my AWS console (at this link.) I clicked ‘Create Function’ and authored a Lambda from scratch (the default). I named my Lambda ‘contactForm’ to indicate what the function controls and chose a runtime environment of Node.js 6.10. I created a custom IAM role and added the below permissions to it which allowed for logging to CloudWatch and sending emails through the Simple Email Service.

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

When I clicked create function, I was redirected to the page for my Lambda, added the below code to it (including my email address as both the sender and receiver) and saved it.

var AWS = require('aws-sdk')
var ses = new AWS.SES()
 
var RECEIVER = /* my email address */
var SENDER = /* my email address */
 
exports.handler = function (event, context, callback) {
    sendEmail(event, function (err, data) {
        context.done(err, null)
    })
}
 
function sendEmail (event, done) {
    var params = {
        Destination: {
            ToAddresses: [
                RECEIVER
            ]
        },
        Message: {
            Body: {
                Text: {
                    Data: 'Message from: ' + event.name + '\nEmail: ' + event.email + '\nMessage: ' + event.message,
                    Charset: 'UTF-8'
                }
            },
            Subject: {
                Data: 'Website Contact Form: ' + event.name,
                Charset: 'UTF-8'
            }
        },
        Source: SENDER
    }
    ses.sendEmail(params, done)
}

Instead of creating API endpoint through the Lambda, I created it in the API Gateway so I could customize it more. This is found under ‘API Gateway’ in the AWS console or here. After selecting ‘Create API’, I named my API after my website domain so I could group it with other endpoints I create for my website in the future. I was prompted to choose between an Edge Optimized and Regional endpoint (differences are outlined here). Edge Optimized endpoints tend to work better for geographically diverse users. Mine is an Edge Optimized endpoint.

I created a new resource named ‘contact’ to indicate it had to do with receiving the contact form and added a POST method for it. The resource prompted me to select an integration type, so I was able to select Lambda and choose the function I just created. From the dropdown Actions menu above the resources, I selected ‘Deploy API’ to make the endpoint live. I had to create a new deployment stage I named ‘prod’. I was then directed to a new view where I could see all my stages. Since I wanted to view the endpoint url, I dropped down the ‘prod’ stage and clicked on my post method so I could find the ‘invoke url’ for it. This is the url that I need to post my form data to, so I went back and updated that in the JavaScript function called on my contact form submit. For my endpoint CORS was enabled by default. If you experience issues with sending data to the API endpoint, try selecting ‘Enable CORS’ for the Actions menu for the API resource. When I returned to the page for the Lambda, it showed the endpoint I just created was now a trigger for the Lambda.

The final piece was setting SES permission to send emails for my sender email address. In the SES part of my AWS console here I can verify email addresses or entire domains and send emails from them. I just added a single email address, by selecting ‘email address’ under ‘Identity Management’. I then selected ‘Verify a New Email Address’ and typed my email address into a modal. A confirmation email was sent to that address from AWS, and I just clicked a link to authorize AWS to send emails from that address.

Creating a contact form for my static site on AWS was not too complex, but it did take me some time and searching to understand what each piece was doing. I found it helpful to test my API settings by sending requests using Postman. I’ve included links below to two blog posts I found very helpful along the way.

Forms to Emails Using AWS Lambda API Gateway
How to Build a Serverless Contact Form on AWS