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:
- 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.
- 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.
- Amazon DynamoDB: A fast, flexible NoSQL database service. We’ll use it to store all submitted contact form messages.
- 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.
- Amazon S3 (Optional but Recommended): For hosting the static HTML/CSS/JavaScript of your contact form itself.

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.
- Go to the DynamoDB Console: Navigate to the DynamoDB service in your AWS console.
- 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.
- Table name:
- 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.
- Go to the SES Console: Navigate to the Amazon SES service.
- Verify New Email Address: Under “Identities,” click “Email addresses” (or “Domains” if you want to use your custom domain).
- Verify a New Email Address: Enter the email address you want to send emails from (e.g.,
no-reply@yourdomain.com
oryourpersonalemail@example.com
). - 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.
- Go to the Lambda Console: Navigate to the AWS Lambda service.
- 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.
- Function name:
- Create Function: Click “Create function.”
- 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 toses:SendEmail
).
- Search for and attach
- 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).
- 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.
- Go to the API Gateway Console: Navigate to the API Gateway service.
- 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)
- API name:
- Create API: Click “Create API.”
- Create Resource:
- In the API Gateway console, under your new API, click “Actions” -> “Create Resource.”
- Resource Name:
contact
(orsubmit
). - Resource Path:
contact
(matches the name). - Enable “Enable API Gateway CORS” (very important for web forms).
- Click “Create Resource.”
- 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.
- With the
- Enable CORS (If not already enabled for the resource):
- Select your
/contact
resource. - Click “Actions” -> “Enable CORS.”
- Use default settings (Allow
*
forAccess-Control-Allow-Origin
is usually fine for a simple form, but restrict in production if possible). - Click “Enable CORS and replace existing CORS headers.”
- Select your
- Deploy API:
- Click “Actions” -> “Deploy API.”
- Deployment stage:
[New Stage]
- Stage name:
prod
(ordev
). - 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.
- Create
index.html
file:
<!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>
- Replace
YOUR_API_GATEWAY_INVOKE_URL_HERE/contact
with the actual Invoke URL from Step 4.
Step 6: Test Your Serverless Contact Form
- Open
index.html
: Open theindex.html
file in your web browser. - Fill and Submit: Fill out the form with some test data and click “Send Message.”
- Check for Success: You should see a “Form submitted successfully!” message.
- 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. - Check Your Email: Verify that the recipient email address (configured in Lambda) received the notification email.
- 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.