Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

How to build a serverless contact form on AWS

Jun 08, 2023 • 11 Minute Read

Please set an alt value for this image...
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 permissionsNote: 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.