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

Monitoring

CloudWatch Alarms:
- Lambda errors > 1%
- DynamoDB throttling
- SES bounces > 5%
- API Gateway 5xx errors