Contact Form using API GW, Lambda & SES

Creating a basic contact form and receiving the inputs via email using AWS services

Raywon Kari - Published on June 19, 2020 - 9 min read



Background


A while back, I spent a little time to build my personal website to list out my skills, experience etc. As part of that website, I have added a contact me page.

Initially to save time, I have used a third party integration (Getform) to receive information from the contact form. Once the website was up and running, I somehow was not satisfied with the Getform integration, and I wanted to build something on my own.

Therefore, I thought I will integrate AWS services to the contact form, which would send me an email whenever someone fills out the contact me details and submit them. In this blog post, I will share how this integration works, and hopefully by the end of it, you can do the same. 😄

We will need the following resources:

  • API Gateway
  • Lambda Function
  • IAM Role for the lambda Function
  • Integrating API GW & Lambda

Here is the data flow:

architecture

Whenever a user fills the contact form, and clicks submit, the data is then sent via a POST request to the API GW endpoint, which is then proxied to the lambda function, where the data processing is done, and an email is sent to me.

First, lets look on the AWS side, and get the things up and running. When they are setup, we will look at the JS side.



AWS CDK


I used AWS CDK to spin up the resources in AWS programatically (except the SES domain, which I added and validated manually).

To begin with, lets install CDK and create a baseline CDK app by running the following command:

> npm install --global aws-cdk
# after the installation is done,
# verify if it is accessible from the terminal
# check cdk installation
> cdk --version
1.45.0 (build 0cfab15)
# create a directory
> mkdir contact-form && cd contact-form
# Initialise baseline CDK app
> cdk init --language=typescript

After the app is initialized, we should be able to see a similar directory structure:

.
├── README.md
├── bin
│   └── contact-form.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── contact-form-stack.ts
├── node_modules
│ └── ...
├── package-lock.json
├── package.json
├── test
│   └── contact-form.test.ts
└── tsconfig.json

I am not writing any tests for this project, so we can safely remove the test directory and other related dependencies. lib/contact-form-stack.ts is the file which is of interest to us, and that is the file where we will write the necessary infrastructure as code.

Now we have the CDK app setup. Lets create our lambda function code first.



Lambda Function


As soon as the user adds the details in the contact form, and clicks submit, API GW receives the data, and sends it to the lambda function. The data is available to access in the body object in the JSON. Our lambda function will parse that data, does some interpolation and sends it to my email address. Here you can find more information on API GW payload and how it sends and receives info from client and backend integrations.

In our CDK app created earlier, create a new directory named lambda in the root directory, and create a new file in it named main.go, where we will add the golang code. In order to deploy the golang lambda function, we need to build it, and use the binary file for deploying.

To do that, run the following command in the lambda directory. I have golang installed. If you have not installed Golang, download it from here.

GOOS="linux" go build main.go
# This command will create a binary file named main

The most important thing in our lambda function is receiving the data, parsing it, and sending an email. That will be our logic. I have defined three structs which will be used to hold input data, as well as the response to the API calls. such as:

//MyEvent struct which has input body
type MyEvent struct {
Body string `json:"body"`
}
//Data has the input types
type Data struct {
Name string
Email string
Message string
}
//Response has the request response
type Response struct {
Message string `json:"message"`
}

After that, we need a main function which will be used by the lambda service to invoke our program. so I do like this:

func main() {
lambda.Start(HandleRequest)
}
func HandleRequest(input MyEvent) (Response, error) {
// Here I will parse the input data
// trigger the send email function
// such as
inputJSON := []byte(input.Body)
var body Data
fmt.Println("Unmarshall input")
err := json.Unmarshal(inputJSON, &body)
sendEmail(body.Name, body.Email, body.Message)
}

In the sendEmail function, we will initialise an AWS Client with SES session, use the SendEmail AWS API call with destination email address, source email address, subject, email body, and send the request.

Once the request is sent, depending on the error status, we either return OK or ERROR all the way to the frontend call, which is then shown as an alert in the browser with customized message depending on the response.

Here you can find the full source code for the lambda function.


Deploy


Now that our lambda function code is ready, its time to deploy it to AWS, and spin up necessary infrastructure around it. Since we will be creating API GW, Lambda & IAM related resources, we need to install the relevant CDK packages for it. Run the following commands in the root directory to install them:

npm install --save @aws-cdk/aws-lambda
npm install --save @aws-cdk/aws-apigatewayv2
npm install --save @aws-cdk/aws-iam
# After the installation is done,
# make sure all the versions match with your cdk-core version.
# Otherwise you will see errors
# my package.json looks like this at the time of writing:
# "@aws-cdk/aws-apigatewayv2": "^1.45.0",
# "@aws-cdk/aws-iam": "^1.45.0",
# "@aws-cdk/aws-lambda": "^1.45.0",
# "@aws-cdk/core": "1.45.0",

Lets open lib/contact-form-stack.ts file and start defining our infrastructure.

First, lets add the lambda function. Here is the sample code:

const mylambda = new lambda.Function(this, 'ContactFormHandler', {
runtime: lambda.Runtime.GO_1_X,
code: lambda.Code.fromAsset('lambda'),
functionName: 'AwsContactFormBackendLambda',
handler: 'main',
logRetention: 7,
initialPolicy: [new iam.PolicyStatement({
actions: [
'ses:SendRawEmail',
'ses:SendEmail',
],
resources: ['*'],
})]
});

Then, lets add api-gateway and the lambda integration to it. I am using the latest HTTP API type, instead of the REST API type. HTTP API news release can be found here.

Here is the sample code:

const apigateway = new apigw.HttpApi(this, 'ContactFormApi');
const lambda_integration = new LambdaProxyIntegration({
handler: mylambda
});
apigateway.addRoutes({
path: '/send',
methods: [ HttpMethod.POST ],
integration: lambda_integration
});

Now we are ready to deploy this to AWS. But first, we need to compile this to JS, to do that, simply run tsc in the root directory. Then, since our CDK app has files to process, we need to create an S3 bucket to store them as artifacts, which will then be used by cloudformation underneath. Luckily CDK has an inbuilt command to do that.

We just need to run the following command:

cdk bootstrap --profile raywon
# you need to configure local AWS CLI
# otherwise CDK deploy will fail
# I have configured my personal account
# with the profile name raywon

Now that our preprequisites are ready, we need to deploy the app. To do that, simply do:

cdk deploy --profile raywon

Now that our API GW is deployed, we need to update CORS configuration for the API. Adding CORS configuration, enables the browsers talk to external APIs hosted on different origins. More info on that, can be found Here.

Following is the code update we need to do, to enable CORS on our API:

cors

Once the deployment is done, head over to the AWS console, and fetch the API Gateway endpoint, which we will need in the frontend JS part.

Full source code can be found here.



Frontend


On the frontend side, here are the things we need to look into:

  • What happens when a user clicks submit
  • How to deal with backend responses i.e., if the response is OK and if the response is ERROR
When user clicks submit:

In my contact form, I have made the rows as mandatory, so until and unless the user fills all of them, the submit button will not do anything. Once the user adds the data and clicks submit, the data is then fed to a function. The function will then try to send a POST request to the API GW endpoint.

Here is an example of how we send the request:

let req = new XMLHttpRequest();
req.open('POST', 'APIGATEWAYENDPOINT');
req.send(JSON.stringify(
{
name:`${this.state.name}`,
email:`${this.state.email}`,
message:`${this.state.message}`
}
));
Handling responses:

Once the request is sent to API GW, we will wait for a response, and we either can get OK or an ERROR response.

If the response is OK, we shown an alert that, data is sent, and we clear the data from the contact form, because we want to show to the user that, we have received the data and cleared out the form.

If the response is not OK, we won't clear the data, but show an alert in the browser a generic message to try again after some time.

Here is an example:

if (resp.message === "OK") {
alert("Thanks! I will get back to you asap!")
that.setState({
name: '',
email: '',
message: ''
});
} else {
alert("Something went wrong! Please re-try after some time")
}

You can find the contact form source code snippet here and the project in my github repo here. Live example can be found on my personal website in the contact page here.

If you have any questions/thoughts/feedback, feel free to contact me in any platform you prefer.



Raywon's Blog © 2020 - 2021

Built with Gatsby & React