Building a Serverless Contact Form with AWS SES, Lambda, and DynamoDB (Step-by-Step Tutorial)

Traditional contact forms often require a backend server to process submissions, validate data, and send emails. This introduces overhead in terms of server provisioning, maintenance, and scaling. What if you could handle form submissions, store data, and send emails without managing a single server? With AWS, you can!

This step-by-step tutorial will guide you through building a serverless contact form with AWS SES, Lambda, and DynamoDB. You’ll learn how to create a highly scalable, cost-effective, and maintenance-free solution that allows users to submit messages via an HTML form, saves their data to a database, and sends an email notification – all without the need for a traditional server. It’s the perfect way to create an email contact form without a server.

By the end of this guide, you’ll have a fully functional serverless contact form AWS solution, demonstrating the power and simplicity of the serverless paradigm.

Architecture Overview: Your Serverless Contact Form AWS

Our serverless contact form will leverage the following AWS services:

  1. Amazon API Gateway: The front door for our contact form. It will expose a REST API endpoint that our HTML form will submit data to.
  2. AWS Lambda: The compute service that runs our backend code. It will be triggered by API Gateway, process the form data, save it to DynamoDB, and trigger an email.
  3. Amazon DynamoDB: A fast, flexible NoSQL database service. We’ll use it to store all submitted contact form messages.
  4. Amazon Simple Email Service (SES): A cost-effective, flexible, and scalable email service. We’ll use it to send email notifications (e.g., to your support team) when a new message is submitted.
  5. Amazon S3 (Optional but Recommended): For hosting the static HTML/CSS/JavaScript of your contact form itself.
Serverless Contact Form The Data Flow

Step-by-Step Tutorial: Building Your Email Contact Form Without a Server

Let’s get started building this serverless solution.

Step 1: Set Up DynamoDB Table

First, we need a place to store the contact form submissions.

  1. Go to the DynamoDB Console: Navigate to the DynamoDB service in your AWS console.
  2. Create Table: Click “Create table.”
    • Table name: ContactFormSubmissions (or a name of your choice)
    • Partition key: submissionId (String) – This will be a unique ID for each submission.
    • Sort key (Optional): timestamp (Number) – Useful for ordering and querying.
    • Settings: Keep default settings for now.
  3. Create Table: Click “Create table.” Wait for the table to become active.

Step 2: Verify an Email Identity in SES

AWS SES requires you to verify email addresses or domains before you can send emails from or to them. For this tutorial, we’ll verify a “From” address.

  1. Go to the SES Console: Navigate to the Amazon SES service.
  2. Verify New Email Address: Under “Identities,” click “Email addresses” (or “Domains” if you want to use your custom domain).
  3. Verify a New Email Address: Enter the email address you want to send emails from (e.g., no-reply@yourdomain.com or yourpersonalemail@example.com).
  4. Check Your Inbox: SES will send a verification email to that address. Click the verification link in the email. Once clicked, the status in the SES console should change to “Verified.”

Step 3: Create the AWS Lambda Function

This is the core logic that handles the form data.

  1. Go to the Lambda Console: Navigate to the AWS Lambda service.
  2. Create Function: Click “Create function.”
    • Function name: processContactForm (or a descriptive name)
    • Runtime: Choose a runtime you’re comfortable with (e.g., Node.js 18.x, Python 3.9). We’ll use Node.js for the example code.
    • Architecture: x86_64 (default).
    • Execution role:
      • Select “Create a new role with basic Lambda permissions.”
      • Crucial Step: After creating the function, you’ll need to add permissions to this role for DynamoDB and SES.
  3. Create Function: Click “Create function.”
  4. Configure Lambda Permissions (IAM Role):
    • Once the function is created, go to the “Configuration” tab, then “Permissions.”
    • Click on the Role name (e.g., processContactForm-role-...) to open it in the IAM console.
    • Attach Policies: Click “Add permissions” -> “Attach policies.”
      • Search for and attach AmazonDynamoDBFullAccess (or ideally, AmazonDynamoDBV2FullAccess or a custom policy limited to your table).
      • Search for and attach AmazonSESFullAccess (or a custom policy limited to ses:SendEmail).
    • Best Practice: In a production environment, create a custom IAM policy that grants only the minimum necessary permissions (dynamodb:PutItem for your specific table, ses:SendEmail for your verified “From” address).
  5. Lambda Function Code (Node.js Example):
    • Go back to your Lambda function’s “Code” tab.
    • Replace the default code with the following:
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const ses = new AWS.SES({ region: 'us-east-1' }); // Change to your SES region

exports.handler = async (event) => {
    let response;
    try {
        const formData = JSON.parse(event.body); // Assuming JSON body from API Gateway
        const { name, email, message } = formData;

        // Basic validation
        if (!name || !email || !message) {
            return {
                statusCode: 400,
                headers: { "Access-Control-Allow-Origin": "*" },
                body: JSON.stringify({ message: 'Name, email, and message are required.' }),
            };
        }

        // 1. Store data in DynamoDB
        const submissionId = AWS.util.uuid.v4(); // Generate unique ID
        const timestamp = Date.now();

        const paramsDb = {
            TableName: 'ContactFormSubmissions', // Your DynamoDB table name
            Item: {
                submissionId: submissionId,
                name: name,
                email: email,
                message: message,
                timestamp: timestamp
            }
        };

        await dynamodb.put(paramsDb).promise();
        console.log('Successfully saved to DynamoDB:', paramsDb.Item);

        // 2. Send email notification via SES
        const paramsSes = {
            Destination: {
                ToAddresses: ['your-recipient-email@example.com'] // Email to receive notifications
            },
            Message: {
                Body: {
                    Text: {
                        Data: `New Contact Form Submission:\n\nName: ${name}\nEmail: ${email}\nMessage: ${message}`
                    }
                },
                Subject: {
                    Data: `New Contact Form Message from ${name}`
                }
            },
            Source: 'your-verified-ses-email@example.com' // Your SES verified "From" email
        };

        await ses.sendEmail(paramsSes).promise();
        console.log('Successfully sent email:', paramsSes.Message.Subject.Data);

        response = {
            statusCode: 200,
            headers: {
                "Access-Control-Allow-Origin": "*", // Required for CORS
                "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
                "Access-Control-Allow-Headers": "Content-Type"
            },
            body: JSON.stringify({ message: 'Form submitted successfully!' }),
        };

    } catch (error) {
        console.error('Error processing form:', error);
        response = {
            statusCode: 500,
            headers: { "Access-Control-Allow-Origin": "*" },
            body: JSON.stringify({ message: 'Failed to submit form.', error: error.message }),
        };
    }
    return response;
};

  • Remember to replace:
    • your-recipient-email@example.com with the email address you want notifications sent to.
    • your-verified-ses-email@example.com with the email address you verified in SES (Step 2) as the “From” address.
    • ContactFormSubmissions with your DynamoDB table name.
    • us-east-1 with your AWS region if different.
  • Save: Click “Deploy” to save your Lambda function.

Step 4: Configure API Gateway

This will create a public endpoint for your form to submit data to.

  1. Go to the API Gateway Console: Navigate to the API Gateway service.
  2. Create API: Choose “REST API” (or “REST API (private)” if desired for internal use cases) and click “Build.”
    • API name: ContactFormAPI (or a descriptive name)
    • Endpoint type: Regional (default)
  3. Create API: Click “Create API.”
  4. Create Resource:
    • In the API Gateway console, under your new API, click “Actions” -> “Create Resource.”
    • Resource Name: contact (or submit).
    • Resource Path: contact (matches the name).
    • Enable “Enable API Gateway CORS” (very important for web forms).
    • Click “Create Resource.”
  5. Create Method (POST):
    • With the /contact resource selected, click “Actions” -> “Create Method.”
    • Select POST and click the checkmark.
    • Integration type: Lambda Function.
    • Lambda Region: Your AWS region.
    • Lambda Function: Start typing the name of your Lambda function (processContactForm) and select it.
    • Click “Save.”
    • When prompted, allow API Gateway to invoke your Lambda function.
  6. Enable CORS (If not already enabled for the resource):
    • Select your /contact resource.
    • Click “Actions” -> “Enable CORS.”
    • Use default settings (Allow * for Access-Control-Allow-Origin is usually fine for a simple form, but restrict in production if possible).
    • Click “Enable CORS and replace existing CORS headers.”
  7. Deploy API:
    • Click “Actions” -> “Deploy API.”
    • Deployment stage: [New Stage]
    • Stage name: prod (or dev).
    • Click “Deploy.”
    • After deployment, you’ll see an “Invoke URL” at the top (e.g., https://xxxxxxx.execute-api.us-east-1.amazonaws.com/prod). Copy this URL – you’ll need it for your HTML form!

Step 5: Create the HTML Contact Form

Now, let’s create a simple HTML form that submits data to your API Gateway endpoint. You can host this HTML file on Amazon S3 for a fully serverless website.

  1. Create index.html file:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Serverless Contact Form</title>
    <style>
        body { font-family: Arial, sans-serif; background-color: #f4f4f4; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
        .container { background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 500px; }
        h2 { text-align: center; color: #333; margin-bottom: 25px; }
        label { display: block; margin-bottom: 8px; color: #555; font-weight: bold; }
        input[type="text"], input[type="email"], textarea {
            width: calc(100% - 20px);
            padding: 10px;
            margin-bottom: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
            box-sizing: border-box; /* Include padding in width */
            font-size: 1em;
        }
        textarea { resize: vertical; min-height: 100px; }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 12px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 1.1em;
            width: 100%;
            transition: background-color 0.3s ease;
        }
        button:hover { background-color: #45a049; }
        #responseMessage {
            margin-top: 20px;
            padding: 10px;
            border-radius: 5px;
            text-align: center;
            display: none; /* Hidden by default */
        }
        .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
    </style>
</head>
<body>
    <div class="container">
        <h2>Contact Us</h2>
        <form id="contactForm">
            <label for="name">Name:</label>
            <input type="text" id="name" name="name" required>

            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>

            <label for="message">Message:</label>
            <textarea id="message" name="message" required></textarea>

            <button type="submit">Send Message</button>
        </form>
        <div id="responseMessage"></div>
    </div>

    <script>
        const form = document.getElementById('contactForm');
        const responseMessage = document.getElementById('responseMessage');

        form.addEventListener('submit', async (e) => {
            e.preventDefault(); // Prevent default form submission

            const name = document.getElementById('name').value;
            const email = document.getElementById('email').value;
            const message = document.getElementById('message').value;

            // **IMPORTANT: Replace with your API Gateway Invoke URL**
            const apiUrl = 'YOUR_API_GATEWAY_INVOKE_URL_HERE/contact'; 

            try {
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ name, email, message }),
                });

                const data = await response.json();

                if (response.ok) {
                    responseMessage.className = 'success';
                    responseMessage.textContent = data.message || 'Form submitted successfully!';
                    form.reset(); // Clear the form
                } else {
                    responseMessage.className = 'error';
                    responseMessage.textContent = data.message || 'Error submitting form. Please try again.';
                }
            } catch (error) {
                console.error('Network or CORS error:', error);
                responseMessage.className = 'error';
                responseMessage.textContent = 'Network error. Please check your connection or CORS settings.';
            }
            responseMessage.style.display = 'block'; // Show message
        });
    </script>
</body>
</html>

  1. Replace YOUR_API_GATEWAY_INVOKE_URL_HERE/contact with the actual Invoke URL from Step 4.

Step 6: Test Your Serverless Contact Form

  1. Open index.html: Open the index.html file in your web browser.
  2. Fill and Submit: Fill out the form with some test data and click “Send Message.”
  3. Check for Success: You should see a “Form submitted successfully!” message.
  4. Verify Data in DynamoDB: Go to the DynamoDB console, select your ContactFormSubmissions table, and click on the “Explore items” tab. You should see your new submission.
  5. Check Your Email: Verify that the recipient email address (configured in Lambda) received the notification email.
  6. Troubleshoot (if needed):
    • Lambda Logs: Check CloudWatch Logs for your Lambda function for any errors or messages.
    • API Gateway Logs: Enable API Gateway CloudWatch logs (as discussed in a previous article!) for detailed request/response tracing if you’re getting 5xx errors.
    • Browser Console: Check your browser’s developer console for JavaScript errors or network issues (especially CORS errors).

Conclusion: Your Serverless Email Contact Form Without a Server is Ready!

You’ve successfully completed building a serverless contact form with AWS SES, Lambda, and DynamoDB! This step-by-step tutorial demonstrates how a combination of powerful AWS services can create a robust, scalable, and cost-efficient backend for common web functionalities.

This serverless contact form AWS solution eliminates the need for managing virtual servers, reduces operational overhead, and automatically scales to handle any traffic volume. It’s a prime AWS SES Lambda DynamoDB example of how serverless architectures can simplify development and maintenance, allowing you to focus on building features rather than managing infrastructure. Now you have a fully functional email contact form without a server, showcasing the efficiency and elegance of serverless design.

🚀 Explore Popular Learning Tracks