Serverless Form Handling at Scale
Written by Terrell Flautt on October 2, 2025
· 8 min · snapitforms.com
Architecture
S3 Static Site → API Gateway → Lambda → DynamoDB
↓
SES Email → SNS Notifications
Core Challenge
Problem: Handle form submissions without backend coding for users.
Solution: Serverless API that receives any form POST and processes intelligently.
Lambda Function Core
exports.handler = async (event) => {
const formData = JSON.parse(event.body);
// Rate limiting
await checkRateLimit(formData.access_key);
// Store submission
await dynamoDB.put({
TableName: 'submissions',
Item: {
id: uuidv4(),
formKey: formData.access_key,
data: formData,
timestamp: Date.now(),
ip: event.requestContext.identity.sourceIp
}
}).promise();
// Send email
await ses.sendEmail({
Source: 'forms@snapitforms.com',
Destination: { ToAddresses: [formData.email] },
Message: {
Subject: { Data: formData.subject || 'New Form Submission' },
Body: { Text: { Data: formatFormData(formData) }}
}
}).promise();
return { statusCode: 200, body: JSON.stringify({ success: true }) };
};
Key Fixes
CORS Issues
Problem: Forms failed from different domains.
Fix: Dynamic CORS headers based on origin whitelist.
const headers = {
'Access-Control-Allow-Origin': getAllowedOrigin(event.headers.origin),
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
};
Rate Limiting
Problem: Spam submissions draining SES quota.
Fix: DynamoDB TTL-based rate limiting.
await dynamoDB.put({
TableName: 'rate_limits',
Item: {
key: `${formKey}_${ip}`,
count: 1,
ttl: Math.floor(Date.now() / 1000) + 3600 // 1 hour
},
ConditionExpression: 'attribute_not_exists(#key) OR #count < :limit'
}).promise();
Email Formatting
Problem: Form data arrived as unreadable JSON.
Fix: Smart formatting based on field names.
function formatFormData(data) {
const formatted = Object.entries(data)
.filter(([key]) => !['access_key', 'subject'].includes(key))
.map(([key, value]) => `${key.replace('_', ' ').toUpperCase()}: ${value}`)
.join('\n');
return formatted;
}
Scaling
- Auto-scaling: Lambda handles 0-1000+ concurrent requests
- Cost: $0.02 per 1000 submissions
- Performance: 200ms average response time
- Reliability: 99.9% uptime with DLQ handling
Monitoring
CloudWatch Alarms:
- Lambda errors > 1%
- DynamoDB throttling
- SES bounces > 5%
- API Gateway 5xx errors