A serverless contact form backend built on AWS β Lambda, SNS, and API Gateway β provisioned with Terraform and deployed automatically via GitHub Actions.
Client (HTML Form)
β
βΌ POST /send-email
βββββββββββββββββββββββ
β API Gateway (HTTP) β β throttled: 10 req/s, burst 5
βββββββββββ¬ββββββββββββ
β AWS_PROXY
βΌ
βββββββββββββββββββββββ
β Lambda Function β β Node.js 22.x
β (send-email) β validates + sanitizes input
βββββββββββ¬ββββββββββββ
β sns:Publish
βΌ
βββββββββββββββββββββββ
β SNS Topic β
βββββββββββ¬ββββββββββββ
β email subscription
βΌ
π§ Your Inbox
.
βββ .github/
β βββ workflows/
β βββ deploy.yml # CI/CD pipeline
βββ send-email/
β βββ index.js # Lambda handler
β βββ package.json
β βββ package-lock.json
βββ terraform/
βββ main.tf # All AWS resources
βββ variables.tf
βββ outputs.tf
- Input validation β email format, name/subject (1β100 chars), message (1β1000 chars)
- Input sanitization β all fields HTML-escaped before publishing
- CORS configured β API Gateway accepts cross-origin requests
- Rate limiting β 10 requests/second, burst of 5
- CloudWatch logging β 14-day log retention for the Lambda
- Automated deploys β push to
mainβ build β plan β apply
- AWS account with programmatic access
- Terraform β₯ 1.5.0
- Node.js 22.x
- GitHub repository with Actions enabled
In your repository go to Settings β Secrets and variables β Actions and add:
| Secret | Description |
|---|---|
AWS_ACCESS_KEY_ID |
IAM user access key |
AWS_SECRET_ACCESS_KEY |
IAM user secret key |
The IAM user needs permissions for Lambda, SNS, IAM, API Gateway, CloudWatch, and S3 (for Terraform state if remote).
The notification email defaults to samuelgiordano@live.com. To change it, edit terraform/variables.tf:
variable "email_endpoint" {
default = "your@email.com"
}Or pass it at apply time:
terraform apply -var="email_endpoint=your@email.com"After the first deploy, AWS sends a confirmation email to the configured address. You must click the confirmation link before any notifications will be delivered.
Push to main and the GitHub Actions workflow handles everything:
- Installs Lambda dependencies
- Zips the function code
- Configures AWS credentials
- Runs
terraform init β plan β apply
# Install Lambda dependencies and zip
cd send-email
npm install
zip -r ../terraform/lambda_function_payload.zip .
# Provision infrastructure
cd ../terraform
terraform init
terraform plan
terraform applyEndpoint: {api_endpoint}/prod/send-email
The API Gateway URL is printed as a Terraform output after deployment:
terraform output api_endpointRequest body:
{
"name": "Jane Doe",
"email": "jane@example.com",
"subject": "Hello there",
"message": "Your message here."
}Validation rules:
| Field | Rules |
|---|---|
email |
Must be a valid email address |
name |
1β100 characters |
subject |
1β100 characters |
message |
1β1000 characters |
Responses:
| Status | Body | Meaning |
|---|---|---|
200 |
"Email sent successfully!" |
Message published to SNS |
500 |
"Internal server error" |
Validation failed or AWS error |
Example request:
curl -X POST https://{api-id}.execute-api.us-east-1.amazonaws.com/prod/send-email \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"email": "jane@example.com",
"subject": "Hello",
"message": "Reaching out from your site."
}'All resources are defined in terraform/main.tf.
| Resource | Name | Notes |
|---|---|---|
| SNS Topic | email-notifications |
Fan-out to email |
| SNS Subscription | β | Email protocol |
| IAM Role | email-lambda-role |
Least-privilege |
| Lambda | send-email-function |
Node.js 22.x |
| CloudWatch Log Group | /aws/lambda/send-email-function |
14-day retention |
| API Gateway | EmailAPI |
HTTP API (v2) |
| API Stage | prod |
Auto-deploy enabled |
Terraform outputs:
terraform output api_endpoint # API Gateway base URL
terraform output sns_topic_arn # SNS topic ARNcd send-email
npm install
# Test the handler locally with a mock event
node -e "
const handler = require('./index');
handler.handler({
body: JSON.stringify({
name: 'Test',
email: 'test@example.com',
subject: 'Hello',
message: 'Testing locally'
})
}).then(console.log);
"Note: Local runs will fail at the SNS publish step unless
SNS_TOPIC_ARNis set and valid AWS credentials are available in the environment.
- All user input is validated with the
validatorlibrary before processing - All fields are HTML-escaped via
validator.escape()before being included in the SNS message - The Lambda IAM role follows least privilege β it can only publish to its own SNS topic and write CloudWatch logs
- CORS is currently set to
allow_origins = ["*"]; restrict this to your domain in production - Consider adding an API key or WAF in front of API Gateway for production workloads
| Package | Version | Purpose |
|---|---|---|
aws-sdk |
^2.1692.0 | AWS service client (SNS) |
validator |
^13.12.0 | Input validation & sanitization |
MIT