Lab 03: Build a Serverless Contact Form
A visitor fills out a form on your S3 site → API Gateway receives it → Lambda processes it → SES emails you. No servers. Zero idle cost. Pure event-driven cloud.
Services: S3 · Lambda · API Gateway · SES · IAM Cost: ~$0 Free Tier Time: 2–4 hours Level: Beginner+
Overview
In this lab you will build a real, working serverless contact form. A visitor fills out a form on your S3-hosted website, clicks Submit, and within seconds you receive an email in your inbox. No web servers. No monthly EC2 bill. Just cloud services working together.
Architecture Flow
- User visits your S3-hosted HTML page and fills in the contact form
- JavaScript sends the form data as a POST request to your API Gateway endpoint
- API Gateway receives the request and triggers your Lambda function
- Lambda reads the form data and calls Amazon SES to send an email
- SES delivers the email to your verified inbox
- The user sees a success message on the page
AWS Services Used
| Service | Role in This Lab | Free Tier |
|---|---|---|
| Amazon S3 | Hosts your static HTML contact form page | Yes |
| AWS Lambda | Serverless function that processes the form submission | Yes |
| Amazon API Gateway | Creates the HTTPS endpoint your form posts data to | Yes |
| Amazon SES | Simple Email Service — sends the email to your inbox | Yes |
| AWS IAM | Grants Lambda permission to call SES securely | Yes |
Prerequisites
- An active AWS account (free tier is sufficient)
- Completed Lab 01 — you have a working S3 static website
- A working email address you can verify in SES
SES requires you to verify email addresses before you can send to them. This confirms you own the address.
- Search for
SESin the AWS Console and click Simple Email Service - In the left sidebar click Verified identities
- Click Create identity → Identity type: Email address
- Enter the email address where you want to receive contact form notifications
- Click Create identity
- Open your email inbox and click the verification link in the email from AWS
- Return to the SES console and refresh — status should show Verified
Lambda needs permission to call SES. You will create an IAM role and attach the necessary policies.
- Search for
IAMin the AWS Console and click it - Left sidebar → Roles → Create role
- Trusted entity type: AWS service → Use case: Lambda → Next
- Search for and add
AmazonSESFullAccess - Search for and add
AWSLambdaBasicExecutionRole(allows Lambda to write logs to CloudWatch) - Click Next → Role name:
LambdaSESRole→ Create role
AWSLambdaBasicExecutionRole policy is essential for debugging — it lets Lambda write execution logs to CloudWatch so you can see what happened if the function fails.Lambda is the core of this project. The function receives form data from API Gateway, formats it into an email, and uses SES to send it to you.
Create the function
- Search for
Lambdain the AWS Console and click it - Click Create function → Author from scratch
- Function name:
ContactFormHandler - Runtime: Python 3.12
- Expand Change default execution role → Use an existing role → select
LambdaSESRole - Click Create function
Add the function code
In the Code source section, delete all existing code and replace it with the following. Replace both email addresses with the address you verified in Step 1.
import json
import boto3
ses = boto3.client('ses', region_name='us-east-1')
SENDER_EMAIL = 'your-verified-email@example.com' # replace this
RECIPIENT_EMAIL = 'your-verified-email@example.com' # replace this
def lambda_handler(event, context):
try:
body = json.loads(event.get('body', '{}'))
name = body.get('name', 'Unknown')
email = body.get('email', 'Unknown')
message = body.get('message', 'No message provided')
ses.send_email(
Source=SENDER_EMAIL,
Destination={'ToAddresses': [RECIPIENT_EMAIL]},
Message={
'Subject': {'Data': f'New contact from {name}'},
'Body': {
'Text': {
'Data': f'Name: {name}\nEmail: {email}\nMessage: {message}'
}
}
}
)
return {
'statusCode': 200,
'headers': {'Access-Control-Allow-Origin': '*'},
'body': json.dumps({'message': 'Email sent successfully!'})
}
except Exception as e:
print(f'Error: {str(e)}')
return {
'statusCode': 500,
'headers': {'Access-Control-Allow-Origin': '*'},
'body': json.dumps({'error': str(e)})
}
'your-verified-email@example.com' with the email you verified in Step 1. The function will fail if these are not updated.Deploy and test
- Click Deploy to save the code
- Click Test → Create new test event → Name:
TestContact - Replace the test JSON with the following and click Test:
{
"body": "{"name": "Test User", "email": "test@example.com", "message": "Hello from Lambda!"}",
"httpMethod": "POST"
}
- You should see a green Execution result: succeeded banner
- Check your email inbox — you should have received the test email
API Gateway creates a public HTTPS URL your contact form will POST data to. It connects the internet to your Lambda function.
- Search for
API Gatewayin the AWS Console and click it - Click Create API → under HTTP API click Build
- Click Add integration → select Lambda → select
ContactFormHandler - API name:
ContactFormAPI→ click Next - Method: POST Resource path:
/contact→ click Next - Stage name:
prod→ click Next → Create - On the API summary page copy the Invoke URL and save it
Enable CORS
- In the left sidebar click CORS
- Access-Control-Allow-Origin:
* - Access-Control-Allow-Methods:
POST, OPTIONS - Click Save
*.Create a file named contact.html on your computer with the code below. Replace YOUR_API_GATEWAY_URL with the Invoke URL from Step 4.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Contact Us</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px;
margin: 60px auto; padding: 0 20px; background: #f2f2f2; }
h1 { color: #1e2a3a; }
label { display: block; margin-bottom: 4px; font-size: 0.9rem; }
input, textarea { width: 100%; padding: 10px; margin-bottom: 14px;
border: 1px solid #ccc; font-family: Arial, sans-serif; }
button { background: #1e2a3a; color: #fff; border: none;
padding: 10px 24px; font-size: 0.9rem; cursor: pointer; }
button:hover { background: #2c3e50; }
#status { margin-top: 12px; font-size: 0.9rem; }
</style>
</head>
<body>
<h1>Contact Us</h1>
<label>Name</label>
<input type="text" id="name" placeholder="Your name"/>
<label>Email</label>
<input type="email" id="email" placeholder="your@email.com"/>
<label>Message</label>
<textarea id="message" rows="5"></textarea>
<button onclick="submitForm()">Send Message</button>
<p id="status"></p>
<script>
const API_URL = 'YOUR_API_GATEWAY_URL/contact'; // <-- replace this
async function submitForm() {
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
const message = document.getElementById('message').value;
const status = document.getElementById('status');
if (!name || !email || !message) {
status.style.color = 'red';
status.textContent = 'Please fill in all fields.';
return;
}
status.style.color = 'gray';
status.textContent = 'Sending...';
try {
await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message })
});
status.style.color = 'green';
status.textContent = 'Message sent! Check your inbox.';
} catch (e) {
status.style.color = 'red';
status.textContent = 'Error sending. Please try again.';
}
}
</script>
</body>
</html>
'YOUR_API_GATEWAY_URL/contact' with the full Invoke URL from Step 4, e.g. https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/contactUpload to S3 and test
- Go to S3 → your bucket → Upload → add
contact.html→ Upload - Open your site URL and add
/contact.htmlat the end - Fill in the form and click Send Message
- Check your inbox — you should receive the email within seconds
What You Learned
- Serverless compute — Lambda runs your code only when triggered, with no server to manage or pay for at idle
- Event-driven architecture — a user action (form submit) triggers a chain of AWS services automatically
- IAM roles and least privilege — you granted Lambda only the permissions it needed, nothing more
- Managed services — SES, API Gateway, and Lambda are fully managed; AWS handles patching, scaling, and availability
- Pay-per-use pricing — you only pay when the function runs; at low volumes this is effectively free
- CORS — Cross-Origin Resource Sharing controls which websites can call your API
Lab Cleanup — Delete Your Resources
| # | Resource | How to Delete |
|---|---|---|
| 1 | Lambda Function | Lambda → Functions → select ContactFormHandler → Actions → Delete |
| 2 | API Gateway | API Gateway → APIs → select ContactFormAPI → Actions → Delete |
| 3 | SES Verified Identity | SES → Verified identities → select email → Delete identity |
| 4 | IAM Role | IAM → Roles → search LambdaSESRole → Delete |
| 5 | S3 contact.html | S3 → your bucket → select contact.html → Delete |
| 6 | CloudWatch Logs (optional) | CloudWatch → Log groups → delete /aws/lambda/ContactFormHandler |