Skip to content

dadsonday/Contact-Form-Serverless-Project

Repository files navigation

Serverless Static Website with a Contact Form (S3 → API Gateway → Lambda → SES) — A Complete, Real-World Guide

Building a serverless contact form is one of the most common use cases for AWS — yet many tutorials gloss over the parts that actually break things in production: CORS, Lambda to API Gateway wiring, IAM permissions, and SES sandbox rules.

S3 static website → API Gateway → Lambda → SES email delivery image

Architecture Overview The goal is to provide a robust contact form without maintaining any traditional servers.

You host your static site (HTML/CSS/JS) on Amazon S3. The contact form submits a POST /submit request to API Gateway. API Gateway invokes an AWS Lambda function using Lambda proxy integration. Lambda sends an email through Amazon Simple Email Service (SES). Lambda returns a JSON response with the necessary CORS headers so the browser accepts the successful response. This is simple, secure, and has zero servers to maintain.

Prerequisites Before starting, ensure you have the following ready in your AWS account (using us-east-1 in this example):

AWS Permissions: Access to S3, API Gateway, Lambda, IAM, and SES. SES Verification: The sender email is verified. If your SES account is still in the sandbox, the recipient email must also be verified. Configure the S3 Static Website Create the Bucket and Policy Go to S3 → Create bucket. Give it a unique name (e.g., contform-s3-website-us-east-1.amazonaws.com).

image

Disable “Block all public access” (you need this for static hosting). Apply a Bucket policy for public read access (replace the bucket name):

{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::my-contact-site-12345/"] } ] }

Enable Static Hosting Go to the Bucket → Properties → Static website hosting → Enable. Set Index document: index.html Configure SES (Email Service) This step ensures you’re authorized to send emails.

In SES → Verified identities, verify both the sender email (required) and the recipient email (if you are in the SES sandbox). You will receive a verification email to the address you enter. Click the link to confirm.

image image

Create the Lambda Function This Python function will receive the contact for data and send the email via SES.

Create IAM Role This role gives the Lambda function permission to interact with other AWS services, specifically SES and CloudWatch Logs.

IAM → Roles → Create role. Trusted entity type: AWS service. Use case: Select Lambda. In Add permissions, search for ses and select AmazonSESFullAccess. Note: For production, use a more restrictive policy, but AmazonSESFullAccess simplifies the initial setup.

image image image image

Create the Lambda Function Lambda → Create function. Name: ContactFormSender. Runtime: Python 3.11 (or latest). Execution role: Choose the IAM role you just created (lambda-ses-send-role). Add these Environment variables to the Lambda’s configuration:

Key,Value,Description SENDER_EMAIL,your verified SES sender,Email used as the source of the message. RECIPIENT_EMAIL,the business owner's email,Email where the contact form submission will go. SES_REGION,us-east-1,The AWS region where SES is configured.

image image image image

Lambda Code (Python) Replace the code in lambda_function.py with the following. Pay close attention to the CORS headers in the _response function, which are crucial for the frontend

import json import boto3 import logging import os from botocore.exceptions import ClientError

logger = logging.getLogger() logger.setLevel(logging.INFO)

SES_REGION = os.environ.get("SES_REGION", "us-east-1") SENDER = os.environ.get("SENDER_EMAIL") RECIPIENT = os.environ.get("RECIPIENT_EMAIL")

ses = boto3.client('ses', region_name=SES_REGION)

def lambda_handler(event, context): logger.info("Event: %s", event)

try:
    # API Gateway uses Lambda Proxy Integration, so the body is a string
    body = event.get("body")
    data = json.loads(body)
except Exception:
    return _response(400, {"error": "Invalid JSON body"})

name = (data.get("name") or "").strip()
sender_email = (data.get("email") or "").strip()
message = (data.get("message") or "").strip()

if not name or not sender_email or not message:
    return _response(400, {"error": "name, email and message are required"})

subject = f"Contact form submission from {name}"
body_text = f"Name: {name}\nEmail: {sender_email}\n\nMessage:\n{message}"

try:
    ses.send_email(
        Source=SENDER,
        Destination={'ToAddresses': [RECIPIENT]},
        Message={
            'Subject': {'Data': subject},
            'Body': {'Text': {'Data': body_text}}
        },
        ReplyToAddresses=[sender_email]
    )
except ClientError:
    logger.exception("SES send failed")
    return _response(500, {"error": "Failed to send email"})

return _response(200, {"message": "Email sent"})

def _response(status_code, body): # For browsers (CORS): include Access-Control-Allow-Origin header # Allow site origin. The specific origin is more secure than "*". return { "statusCode": status_code, "headers": { "Content-Type": "application/json", "Access-Control-Allow-Origin": "http://contform-s3-website-us-east-1.amazonaws.com", "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", "Access-Control-Allow-Methods": "OPTIONS,POST" }, "body": json.dumps(body) }

Replace the Access-Control-Allow-Origin value with your actual S3 static website endpoint.

image
Create API Gateway (REST API)

While HTTP API is simpler, we’ll demonstrate a REST API setup for maximum control over CORS, as shown in your images.

Create the API API Gateway → Create API → REST API. Choose New API. API name: lambses.

image

Create Resource and Method Select the root resource (/) and Create resource. Resource Name: submit. Select the new /submit resource and Create method → POST. Integration type: Lambda Function. Lambda Region: us-east-1. Lambda Function: ContactFormSender.

image

Enable CORS on the Resource CORS is mandatory for the browser to allow the cross-domain request from S3 to API Gateway.

Select the /submit resource. Click Actions → Enable CORS. 3. Configure the CORS settings: * Gateway responses: Check Default 4XX and Default 5XX. * Access-Control-Allow-Origin: Enter your S3 website domain exactly (e.g., http://contform-s3-website-us-east-1.amazonaws.com). * Note: In production, do not use *.

  1. Click Save. This action automatically creates the necessary OPTIONS method for pre-flight requests.
image image

Deploy the API Click Actions → Deploy API. Deployment stage: [New Stage] Stage name: prod. Copy the Invoke URL from the stage details — you will use this in your frontend JavaScript.

image image

Frontend: HTML + CSS Form Upload these files to your S3 bucket. Remember to replace YOUR_API_ID.execute-api.us-east-1.amazonaws.com/prod/submit with your actual Invoke URL.

<title>Serverless Contact Form</title>

Send Us a Message

    <form id="contact-form">
        <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="subject">Subject</label>
        <input type="text" id="subject" name="subject" required>

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

        <button type="submit" id="submit-button">Send Message</button>

        <p id="form-status" aria-live="polite"></p>
    </form>
</div>

<script>
    // *** IMPORTANT: REPLACE THIS PLACEHOLDER WITH YOUR DEPLOYED API GATEWAY INVOKE URL ***
    const API_ENDPOINT = 'https://****.execute-api.us-east-1.amazonaws.com/prod/submit';

    const form = document.getElementById('contact-form');
    const statusMessage = document.getElementById('form-status');
    const submitButton = document.getElementById('submit-button');

    form.addEventListener('submit', function(event) {
        // Prevent the default form submission (which would refresh the page)
        event.preventDefault(); 
        
        // Collect form data
        const formData = new FormData(form);
        const payload = {};
        formData.forEach((value, key) => {
            payload[key] = value;
        });

        // Show loading state
        statusMessage.textContent = 'Sending...';
        submitButton.disabled = true;

        fetch(API_ENDPOINT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(payload) // Convert the data object to a JSON string
        })
        .then(response => {
            // Check for a successful response (status 200-299)
            if (response.ok) {
                return response.json(); // Process the JSON response from Lambda/API Gateway
            }
            // Handle HTTP error responses
            throw new Error(`Submission failed: ${response.status} ${response.statusText}`);
        })
        .then(data => {
            // Success
            statusMessage.textContent = '✅ Message sent successfully! We will be in touch soon.';
            form.reset(); // Clear the form fields
        })
        .catch(error => {
            // Error handling
            console.error('Error submitting form:', error);
            statusMessage.textContent = `❌ An error occurred: ${error.message}. Please try again.`;
        })
        .finally(() => {
            // Re-enable the button after process completes
            submitButton.disabled = false;
        });
    });
</script>
image

⚠️ CORS — What Breaks Most People If your browser returns a No 'Access-Control-Allow-Origin' header is present… error, your CORS configuration is wrong.

Checklist for Success API Gateway must have the OPTIONS method configured (done automatically when you click Enable CORS). Lambda must return the correct Access-Control-Allow-Origin header in its response. This is set in the Python code's _response function. The Allowed origin in both API Gateway and Lambda must exactly match your S3 website domain (including the http:// or https://) and must not have a trailing slash. Final Checklist Before Launch SES sender + recipient verified. Lambda IAM role includes ses:SendEmail and CloudWatch Logs permissions. API Gateway CORS enabled on the /submit resource. Lambda returns CORS headers in the response function. S3 website origin matches the Access-Control-Allow-Origin value. Fun Trouble — CORS messed me up like four hours straight… the joy that it eventually worked.

image image

Happy Troubleshooting.

About

Contact Form Serverless Project using s3, lambda, API Gateway and SES

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published