The contact form is critical to all websites, small and large. There are many services available to embed contact forms into your own website, but this tutorial is not about leveraging those services. Instead, it is about learning a handful of Amazon Web Services by focusing on the problem of creating our own contact form architecture. By the end of this guide, we will have created a serverless contact form and learned more about the following services:

  • API Gateway
  • Lambda
  • Simple Email Service

We will create an API Gateway (APIGW) POST endpoint that will take the following JSON request body:

{
“email”: <contact-email>,
“subject”: <contact-subject>,
“message”: <contact-message>
}

This endpoint will have a Lambda function behind it that will turn the request body into an email. That email request will sent to Simple Email Service (SES) for delivery. With those pieces in place, we can then create a contact form that sends the message.

Configuring Simple Email Service to send us email

Before diving into any code, we need to configure Simple Email Service (SES). Our Lambda function is going to process the message from our website and send the email via SES.

SES is a slick offering for both delivering and receiving email. It offers high deliverability, cost effectiveness, and very reliable email processing.

In this post, we are going to leverage it to take a message and send it to our own whitelisted email address. For the purposes of the project we’re creating, we can do everything within the SES sandbox environment. The sandbox environment has the following limitations:

  1. Can only send email to the SES mail box simulator or to email addresses or domains that you have verified.
  2. Limited to sending 200 emails per day.

If you intend to send more than 200 emails per day, you will need to request a sending limit increase. That request removes you from the sandbox, once granted.

For this project, however, you will be fine in the sandbox environment. To configure SES to send emails to yourself, we’ll need to complete the following steps:

  1. Navigate to Simple Email Service in the AWS Console.
  2. Click Email Addresses
  3. Click Verify a New Email Address
  4. Enter the email address you would like to use for contact requests.
  5. Click Verify This Email Address
  6. Check your email for a verification email with subject “Amazon Web Services – Email Address Verification Request”
  7. Click the link in the email to confirm your email address.

Setting up your initial Lambda function to respond to API Gateway triggers

You got your SES setup squared away? Your email address is a verified email address that SES can send email to? Then you’re making excellent progress already.

Since the debut of Lambda functions in November 2014, Amazon Web Services has streamlined their creation. It used to be the case that configuring a Lambda for different AWS events was tedious and error prone. Today, the initial setup is more streamlined and user friendly. This is great for us, but do know that we will have to do some further tweaking later on.

But first, let’s get our initial Lambda set up and triggered from an API Gateway endpoint.

The backbone of the serverless stack is an event. An event kicks off the allocation of compute resources to complete some action. A trigger in AWS is the event that will allocate a container to execute the code in your Lambda function. Within AWS there are currently 17 different events that Lambda can respond to.

Today, we are leveraging the API Gateway event via an HTTP endpoint. Think of the HTTP endpoint as triggering an event for our Lambda function. The request starts the container and includes the input event to our Lambda function. Using this input, we can process the message in our code.

How about we jump in and configure the initial endpoint and Lambda function:

  1. Navigate to Lambda from the AWS Console.
  2. Click Create function
  3. In the Blueprints input, enter “api
  4. Click the blueprint “microservice-http-endpoint” in NodeJs 6.10.
  5. For “API name”, click Enter value and enter “contact
  6. For “Deployment stage”, leave the default “prod” selected.
  7. For “Security”, select “Open
  8. Click Next
  9. Enter “ContactFormLambda” for your function Name.
  10. Enter a meaningful description like “Process APIGW POST to /contact
  11. Select “Node.js 6.10” for the Runtime.
  12. For now, we are going to enter this boilerplate code in the inline-code section:
  13. In the “Lambda function handler and role”, leave Handler at “index.handler”. Role should be “Create new role from template(s)”. For role name, enter “ses-contact-form-lambda”. For policy templates, select “Simple Microservice permissions
    Note: We will do more configuration around these areas later on.
  14. Click Next
  15. Click Create function

By walking through this wizard, we will end up with the following AWS resources:

  • An API Gateway endpoint that will proxy HTTP(S) requests.
  • A Lambda function that your API Gateway endpoint triggers.
  • An IAM role “ses-contact-form-lambda” that has the basic execution policy for Lambda.

There is still further refinement needed, but we now have the resources needed for our serverless contact flow.

Configure your IAM Role to send email via SES

Before we start writing our Lambda function, we need to take a pause and configure the ses-contact-form-lambda IAM role. Currently, the role is not allowed to use the sendEmail API in SES.

To make this very lightweight, we’ll create a new IAM policy that has only the API on SES that we need to grant access too.

  1. Navigate to IAM from the AWS Console.
  2. Click Policies
  3. Click Create Policy
  4. Select “Create Your Own Policy
  5. Enter “contact-form-send-email-policy” for the Name.
  6. Configure the Policy Document as follows:
  7. Click Create Policy

Now there is a granular policy that allows access to the ses:SendEmail API. Attach this policy to the ses-contact-form-lambda role:

  1. Navigate to IAM from the AWS Console.
  2. Click Roles
  3. In search enter “ses-contact-form-lambda
  4. Click “ses-contact-form-lambda” role.
  5. Click Attach Policy
  6. Select “contact-form-send-email-policy
  7. Click Attach Policy

With those IAM changes, the Lambda function now has access to the SendEmail API of SES.

Sending email via SES from your Lambda function

We now have all the finalized infrastructure pieces for your serverless contact flow. That only took a few button clicks and, voila–we have the API Gateway, Lambda function, and IAM role. Pretty slick, right?

The infrastructure, at this point, is not yet ready for primetime. We will make it so in a bit, but first let’s add the code for sending email via SES from your Lambda function. There are no external dependencies in our function. Because of this, we can edit the code inline. If we were creating Lambda functions that require other NPM modules we would need to develop outside of the AWS Console. The reason is because when zip files get uploaded to Lambda, NPM packages must be included.

But that is not the focus of this project. We are going to edit the code inline because we have no external dependencies.

  1. Navigate to Lambda from the AWS Console.
  2. Click the “ContactFormLambda” function.
  3. Enter the following code into the inline editor:

In 56 lines, we have the code necessary to take the API Gateway request, parse out the email message, and then leverage SES to send the email to ourselves. But wait–what the hell does this code do? I’m glad you asked.

Let’s break down the interesting parts and go over what is happening. Starting at the top of the Lambda function, there are three constants declared:

The interesting piece here is that the aws-sdk is not an external dependency. The package is global inside of the Lambda function container and is always available to use. We instantiate a SES client by calling AWS.SES(). We have also declared sesConfirmedAddress. This must be the email address you verified earlier, during your setup of SES.

The function, getEmailMessage, processes the body of the request. It parses the request from the API Gateway request body and builds the sendEmail request. We want to send the email to ourselves, so the ToAddresses is the SES email we verified earlier. The Source must be the email we verified because that is the email doing the sending as well.

The rest we get from the JSON request that the user sent via our API Gateway. The body of the email is in the message property. The subject for the email is in the subject property. The reply-to field of the email is in the email property the user passed in.

This is the meat of our function, the main event handler. The first thing to do is to pull out the request body as that contains the email, message, and subject for the email. Next, get the sendEmailRequest by passing the request to the getEmailMessage function. With the request in hand, we call sesClient.sendEmail(params) API. We append the .promise() method to tell the SDK that we want a promise back.

Tip: With the aws-sdk in JavaScript, you can append .promise() to any SDK call to get back a promise.

The API Gateway endpoint is set as a LAMBDA_PROXY. This means that the Lambda function must build and return an HTTP response. For our purpose, returning a status code of 200 for success and 500 for error is enough. When sendEmailPromise resolves successfully, return a 200. If it fails, the email failed to send, so return a 500.

In either case, we are only changing the statusCode of response and calling callback(null, response). You leverage the callback function in Lambda to signal success or failure:

LAMBDA_PROXY in API Gateway needs an HTTP response from your Lambda function. The invocation is always ended with callback(null, response). A failure to return an HTTP response causes API Gateway to return a 502 Bad Gateway.

Showtime – time to test your endpoint

You updated your Lambda function code to the above. You have sesConfirmedAddress set to the verified SES email. The IAM role for the Lambda has access to the sendEmail API of Simple Email Service. All we have left is to test and see if it works.

As the old adage goes, if you haven’t tested it, then it doesn’t work. For the purposes of this project I am talking about verifying that it works by using it and observing the results. There are other levels of testing that we should do here if we were building a full production app. Unit tests and integration tests would be great things to add. But for this simple contact flow, let’s just make sure it works from our API Gateway.

  1. Navigate to API Gateway from the AWS Console.
  2. Click the “contact” API.
  3. Click ANY under the “/ContactFormLambda” resource.
  4. Click TEST
  5. Select “POST” for Method.
  6. In the Request Body enter the following:
  7. Click Test

On the right hand side, we will see the request flow of API Gateway calling our Lambda function. Here is what we are looking for at the bottom:

  • Method completed with status: 200 — Awesome it worked! Check your email.
  • Method completed with status: 500 — Darn it broke. Time to check the logs.
  • Method completed with status: 502 — Configuration error. Time to check the logs.

If we see the good to go sign–aka, a 200–then we can check our email and make sure it looks the way we expected. If we got anything else, we will need to do some debugging.

The first thing to check when faced with an error? The logs. Where do the logs live though?

For every Lambda invocation, there is an associated CloudWatch log stream. Here’s how to find it:

  1. Navigate to CloudWatch from the AWS Console.
  2. Click Logs
  3. In the “Log Group Name Prefix” enter “/aws/lambda/ContactFormLambda
  4. Click “/aws/lambda/ContactFormLambda

Once we click into the log group, we will see the log streams of our Lambda function. The top one is the most recent invocation. An error in any of your Lambda function invocations is logged in the log stream. In a log stream, we can view the errors. Search for logging statements, and see how much memory and time a given invocation used. So, if we got anything but a 200 response code, start debugging by checking the logs. If the logs don’t reveal any information, then we should review the rest of our setup. Things to keep an eye out for:

  • Typos in things you weren’t expecting
  • Not assigning the right IAM policy to the right IAM role
  • Misconfigured Lambda function–incorrect memory allocation or time allocation

Enable CORS and publish your API

Before your functioning API is ready for the wild wild west, you have a few final knobs to turn.

The first thing we need to do is enable Cross Origin Resource Sharing (CORS) on our API endpoint. This grants endpoint access to domains we specify. The last thing will be to publish our API to a public stage.

First, let’s go ahead and turn CORS on for this endpoint:

  1. Navigate to API Gateway from the AWS Console.
  2. Click the “contact” API.
  3. Click “ContactFormLambda” resource.
  4. Click Actions
  5. Select “Enable CORS
  6. In “Access-Control-Allow-Origin” enter the URL of the website you are going to call this endpoint from. If you are unsure, leave it as ‘*’ which will allow any domain.
  7. Click Enable CORS…
  8. Click Yes, replace existing values

With CORS enabled, the only thing left is publishing the endpoint to a stage (aka an environment).

  1. Navigate to API Gateway from the AWS Console.
  2. Click the “contact” API.
  3. Click “ContactFormLambda” resource.
  4. Click Actions
  5. Click Deploy API
  6. Select “prod” from Deployment stage.
  7. Click Deploy

Once deployed, our API is now living in the prod environment. We’ll have a URL that looks something like:

https://<some-characters>.execute-api.us-west-2.amazonaws.com/prod/ContactFormLambda

Integration

At this point, we have a functional serverless contact flow. API Gateway calls a Lambda function. That function takes the request body and sends it via Simple Email Service.

The next step is to integrate this new API of yours into something of your choosing. You can start with your own website or portfolio page. To integrate it, create a form with the same fields as the request body requires–the user’s email address for replies, the subject, and the body. On form submission, add a quick AJAX request in JavaScript with the request body and you should be off to the races.

Update: We’ve created a follow-up guide that continues from where we left off here, and integrates the service into a web page. You can check it out here.

Conclusion

There is a vast sea of information out there around AWS. So much so that it can be easy to get lost in trying to learn it. The best way to learn anything is to start using it. By following along, you’ve learned the concepts of API Gateway, Lambda, and SES by using them in a practical problem.

This is the way I first learned AWS. It is what I still do today as a Certified Professional Solutions Architect. If you have any questions please feel free to reach out to me. If you want to learn about AWS by creating more projects like this one, check out my upcoming book, How To Host, Deliver, and Secure Static Websites on Amazon Web Services.


Keep up with other projects Kyle is working on by following him on Twitter, LinkedIn, and Medium.

18 responses to “How to build a serverless contact form on AWS”

  1. Riadh says:

    Thank you great article

  2. Kyle Olsen says:

    This is a solid guide, but I do wish more examples were geared toward automation via CloudFormation (with or (preferably) without SAM) or Terraform or the like. I feel like most people aren’t going to be doing these things in real shops through the console.

  3. Joey Mendoza says:

    Great post. Keep them coming.

  4. Cesar Munoz says:

    Great guide especially as I continue to try and improve my skills with AWS. Keep the good work up.

  5. Murtuza Kolasavala says:

    Awesome and very helpful article.

  6. Daniel White says:

    Thanks for this article.

  7. Jay says:

    Having problem with getting this to work the API side works have tested and it’s a success.I followed this steps in (https://www.codeengine.com/articles/process-form-aws-api-gateway-lambda/)setting up API Lambda and SES -all works fine & below is my html code and having trouble posting to API to get it to send please help:

    Full Name:

    Email Address:

    Message:


    Send Message

    function submitToAPI() {
    var URL = ‘/contact’;
    // $(‘#contactform’).submit(function (event) {

    // event.preventDefailt()
    //load destinations
    var data = {
    name: $(‘#name’).val(),
    email: $(‘#email’).val(),
    message: $(‘#message’).val()

    };

    $.ajax({
    url: URL,
    method: “POST”,
    contentType: “application/json; charset=utf-8”,
    dataType: “json”,
    data: JSON.stringify(formJSON),
    cache: false,
    success: function (response) {
    console.log(“Email successfully sent”);
    $(‘#successMessage’).removeClass(“hidden”);
    $(‘#formGroup’).addClass(“hidden”);
    },
    error: function (msg) {
    console.log(“Form error: ” + msg);
    $(‘#errorMessage’).removeClass(“hidden”);
    }
    });
    }

    • Phil Zona says:

      Hey Jay – we just posted a guide that addresses this (using a slightly different method), which you can find here.

      When you say the API is working, I am assuming you mean a test with a tool like Postman or curl. Things work a little differently in the browser, however – if you’re developing this locally, you may have issues with CORS (cross-origin resource sharing). I’m not sure how to address this in jQuery, but I would start doing some research around that.

      Our new guide that I linked to above goes into more detail about this and gives an example. If you’re still having trouble, feel free to reach out to me directly on Twitter or another channel and I’ll see if I can help.

  8. Becki True says:

    Fantastic articles (including the followup article with the code for the form)!!
    I was able to setup my own form for downloading eBooks with an email sign-up. Thanks for the detailed and accurate instructions. I learned a lot!!

  9. Jake says:

    Hi, great article, thanks. Followed it to the letter and have it working via test API, but when testing using sample form provided in your follow on article I get a CORS error: Failed to load : No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ” is therefore not allowed access. The response had HTTP status code 403. This is with the ‘*’ value which should in theory allow any origin. Any thoughts appreciated, thanks.

    • Phil Zona says:

      Hey Jake, I also got this error a few times while I was testing. The main reasons wer not including the resource name or stage name in my URL when making my API call (endpoint variable in the second guide), or not setting the ‘no-cors’ mode in my Lambda request.

      Also worth noting, I tested the form from the second guide on Chrome. Different browsers apply CORS rules differently – have you tried hitting the Lambda function from other sources like curl or Postman?

      • Jake says:

        Thanks for the reply Phil. Adding an ‘Access-Control…’ header in the lambda response variable seemed to fix it initially, although removing it again also now works so who knows. Chrome still reports cors disallowed access for domains that aren’t the one listed under enable cors (as it should), but mail gets through anyway bizarrely.

        • Phil Zona says:

          Strange. I ended up trying something similar while I was writing the follow up guide – I was sending back an ‘Access-Control-Allow-Origin’ header manually from Lambda, but then removed it in the final version since it worked without it. I’ll admit CORS is still something I have a lot to learn about, and there could be factors at play that I didn’t cover very well in the guide. Local testing is especially tricky, and I’ll ask around to see if anyone on our team has any suggestions for how to improve that. Glad you got it working!

  10. James says:

    This results in 403 errors from my localhost tests even when allowed origin is set to ‘*’.

  11. […] from Cloud9. The top section will allow you to test it with a payload. For example, if you wrote a serverless contact form, you might input your form data here to test it. However, since we aren’t accepting any […]

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Get actionable training and tech advice

We'll email you our latest articles up to once per week.