Handling Contact Forms Submissions With a Custom REST API using AWS SES, API Gateway, and Lambda

Handling Contact Forms Submissions With a Custom REST API using AWS SES, API Gateway, and Lambda

Learn how to build a REST API to handle contact form submissions using AWS SES, Lambda, and API Gateway via the AWS CDK. And, how to test it using Postman.

When it comes to building contact forms for a website, you could use a paid service to handle the sending of form submissions to you but in a world where everything is going subscription based do you really want to add one more to your list?

So, in this tutorial, we’ll be exploring how to build a REST API that can process requests and send the contents of those requests as an email to a designated email address. You would then be able to take the URL of this API and make requests to it from a frontend application and allow users to submit contact forms to then be delivered to your inbox. I’ll demonstrate this functionality by using Postman to send requests to our API.

For this project, we’ll be using the same email address to receive the submissions as well as to send them. So, in practice, we’re emailing ourselves the contents of the requests sent to our API but if you wanted to you could update this and use a different receiving email address to the sending one.

Also, for this tutorial, I’ll be using the AWS CDK but if you would rather complete this project using the AWS dashboard, then make sure to check out my past tutorial on this, it also includes a frontend project example for building a contact form.

The AWS services we’ll be using in this tutorial are:

  • API Gateway: For creating our REST API and API key for authentication
  • Lambda: For creating a function to handle the processing of requests and request the sending of emails via SES
  • SES: Sending the emails and configuring our email address to send the emails
  • IAM: To give permission to our Lambda function to allow sending emails using SES.

Now, we know what we’ll be building and what we’ll be building it with we’re almost ready to get started. But, before we get into the tutorial, I just want to mention that you’ll need to have an AWS account created as well as have the AWS CDK and CLI configured on your machine, along with having an existing CDK project. You can learn how to do all of this in under 60 seconds with my tutorial on TikTok here.

Configuring SES to Send Emails

The first thing we need to do in our CDK project is define the email address we want to send our form submissions from, this is called a verified email identity. To create this verified identity, add the below code to our stack definition file in the lib directory; make sure to update YOUR_EMAIL_ADDRESS to the email address you want to send emails from.

// 1. Define our SES Verified Email Address
const verifiedEmail = 'YOUR_EMAIL_ADDRESS';
const identity = Identity.email(verifiedEmail);
new EmailIdentity(this, 'SESIdentity', {
  identity,
});

Adding Our Lambda Processor Function

With SES now configured and ready to go, let’s shift our focus to the Lambda function we’ll be using to process requests from our API and then request the sending of emails with SES. The first thing we want to do is define our Lambda function in our stack definition file, so just below the SES code, add the code below for our Lambda function.

// 2. Create our Lambda functions to handle requests
const sendEmailLambda = new NodejsFunction(this, 'SendEmailLambda', {
  entry: 'resources/send-email.ts',
  handler: 'handler',
  environment: {
    VERIFIED_EMAIL: verifiedEmail,
  },
  initialPolicy: [
    new PolicyStatement({
      actions: ['ses:SendEmail'],
      resources: [
        `arn:aws:ses:${this.region}:${this.account}:identity/${identity.value}`,
      ],
    }),
  ],
});

The two important things to note with this definition are that we pass the email address we configured with SES to our Lambda function as an environment variable so we can access it inside the function. And, secondly, we add a new IAM policy to our Lambda to allow it to send emails using SES with the email address we configured.

With both of those things covered, let’s move on to creating the actual function that our Lambda will execute. To do this, create a new file at ./resources/send-email.ts and then inside it add the below code.

import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { APIGatewayProxyEvent } from 'aws-lambda';

interface IEventBody {
  firstName: string;
  lastName: string;
  email: string;
  message: string;
}

const sesClient = new SESClient({});

export const handler = async (event: APIGatewayProxyEvent) => {
  // If no body, return an error
  if (!event.body) {
    return {
      statusCode: 400,
      body: JSON.stringify({ message: 'Missing body' }),
    };
  }

  const { VERIFIED_EMAIL = '' } = process.env;

  // Get data from the request sent from the frontend that triggered the lambda
  const body = JSON.parse(event.body) as IEventBody;
  const { firstName, lastName, email, message } = body;
  const requiredFields = ['firstName', 'lastName', 'email', 'message'];

  // Check all of the required fields are present in the body
  for (const key of requiredFields) {
    if (!body[key as keyof IEventBody]) {
      return {
        statusCode: 400,
        body: JSON.stringify({ message: `Missing field: ${key}` }),
      };
    }
  }

  // Config for SES to send the email
  const params = {
    // Email address the email is sent to
    Destination: {
      ToAddresses: [VERIFIED_EMAIL],
    },
    Message: {
      // Body of the email
      Body: {
        Text: {
          Data: `
New message:
---
Name:${firstName} ${lastName}
Email: ${email}
Message: ${message}
`,
        },
      },
      // Subject line of the email
      Subject: { Data: `Contact Form Message` },
    },
    // Email address the email is sent from
    Source: VERIFIED_EMAIL,
  };

  // Send the email
  try {
    const response = await sesClient.send(new SendEmailCommand(params));

    if (response.$metadata.httpStatusCode !== 200) {
      return {
        statusCode: 500,
        body: JSON.stringify({ message: 'Error sending email' }),
      };
    }

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Email sent' }),
    };
  } catch (e) {
    return {
      statusCode: 500,
      body: JSON.stringify({ message: e }),
    };
  }
};

In this function, we take the body from the API request and check it contains all of the defined required fields before configuring the parameters required for SES to send our email and finally, sending the actual email.

Inside the lambda, we also do several checks to handle potential errors; for example, the request body not being provided, required fields being missed, or if an error happens with SES sending the email. Each of these will return an error code with a relevant message to our client sending the request.

Finally, at this point, it’s worth remembering that the email we send from and to is the same email address (the one we verified in SES). However, if you wanted, you could configure the receiving email address to be a different email address by changing the array passed to the ToAddresses property in the SES config. But it’s important we keep the Source property as the email address we verified in SES otherwise, the emails won’t send.

Creating an API to trigger our Lambda function

With our Lambda function now defined and written, we’re ready to move onto the final piece of our CDK stack and that’s defining our new REST API. To define our new REST API, add the below code under the Lambda definition we created earlier in the stack definition file in the lib directory.

// 3. Define our REST API
const api = new RestApi(this, 'EmailApi', {
  restApiName: 'EmailApi',
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_ORIGINS,
    allowMethods: Cors.ALL_METHODS,
  },
  apiKeySourceType: ApiKeySourceType.HEADER,
});

// 4. Create our API Key
const apiKey = new ApiKey(this, 'EmailApiKey');

// 5. Create a usage plan and add the API key to it
const usagePlan = new UsagePlan(this, 'EmailUsagePlan', {
  name: 'Email Usage Plan',
  apiStages: [
    {
      api,
      stage: api.deploymentStage,
    },
  ],
});

usagePlan.addApiKey(apiKey);

// 6. Connect our Lambda functions to our API Gateway endpoints
const sendEmailIntegration = new LambdaIntegration(sendEmailLambda);

// 7. Define a POST handler on the root of our API
api.root.addMethod('POST', sendEmailIntegration, {
  apiKeyRequired: true,
});

In this code, we define several things, we start by defining our new REST API before then adding a new API key and a usage plan to connect our API and API key together. We then configure a new LambdaIntegration which is how we connect our API and Lambda function. Before, finally, defining the endpoint to which we want users to send their requests along with the Lambda function we want that endpoint to trigger.

In our case, we’ll be using the root of the API with a POST request which will trigger the Lambda function we created earlier for processing the results.

It’s worth mentioning that creating REST APIs with API Gateway and the AWS CDK can be a complicated subject so to help with this I’ve created a standalone tutorial that goes into a lot more depth than this tutorial, you can check it out here.

Deploying Our New API

We’ve now defined all of the services we need for our new REST API to process requests from a contact form and send the results to our target email address. But, before we can deploy our CDK stack to our AWS account, we need to add one more output to our CDK stack for our API key ID. So, at the bottom of your stack definition file in your lib directory add the below code.

// Misc: Outputs
new cdk.CfnOutput(this, 'API Key ID', {
  value: apiKey.keyId,
});

With that code added we’re ready to deploy our CDK stack, so in the terminal run the command cdk deploy and then accept any prompts you’re given. Once your deployment has finished, you should have two outputs in your terminal that look like the below.

SesSendEmailApiStack.APIKeyID = YOUR_API_KEY_ID
SesSendEmailApiStack.EmailApiEndpoint2C21ACE3 = YOUR_API_URL

Now, we have our API key ID we can get our actual API key by using the AWS CLI and running the command aws apigateway get-api-key --api-key <YOUR_API_KEY_ID> --include-value. Then once we have our API key value from the result shown to us in the terminal, we can perform requests to our API using a tool like Postman which is what we’ll be doing in the next section!

Testing Our New API With Postman

Before we can test our new API, we first need to complete the verification process for the email address we configured with SES at the start of the tutorial. To complete this process, check the inbox of the email address you configured for an email from AWS, inside that email should be a link, if you click that link it will complete the verification process and take you to a page confirming its completion.

With our email address now verified, we’re ready to configure Postman to test our new API. To do this, add our API URL to the URL input field in Postman and select the request method of POST as that’s what we configured our API to use.

We’ll then want to configure our API key to be included in the request by adding a header to the request. To do this, under the “Headers” tab in Postman, add a new entry with the key of x-api-key and the value as your API key.

Finally, we need to add a body to our request, to do this, go to the “Body” tab and select the “raw” option before then adding the below JSON (customize the values as you want).

{
    "firstName": "FIRST_NAME",
    "lastName": "LAST_NAME",
    "email": "EMAIL_ADDRESS",
    "message": "MESSAGE"
}

You can then hit the send button in Postman and within a few moments, you should receive an email in the inbox of the email address you specified at the start of this tutorial (or, if you changed it, the email address you added to the ToAddresses property instead). You should also receive a response in Postman with a status code of 200 and a body of {"message": "Email sent"}.

At this point we know our API works but if you would like to test your API further than a successful request, here are some more tests you could run to test some of the failure conditions we handled inside the function.

  • No API key provided: 403 Forbidden
  • API key and no request body provided: 400 - Missing body
  • API key and missing required fields: 400 - Missing field: MISSING_FIELD_NAME

Closing Thoughts

And, if all of the tests passed successfully then congrats you have a working API that you can use to send contact form messages to a target email address. All you need to do now is send requests to your API URL from your frontend with your API key and a body containing the required fields, the rest is handled for you!

So, I hope you found this tutorial on building a REST API to handle contact form submissions using the AWS CDK helpful and if you’d like to see the full example code for this project, you can see it over on my AWS CDK examples repository here.

And, until next time, thank you for reading.

Coner


This post was originally published on my blog on 30th July 2023  ---