Serverless Contact Form
Part 9 — extending the Cloud Resume Challenge with a contact API.
The footer “Get in touch” form used to call a stale API Gateway URL that was not in this AWS account and did not return CORS headers. Browser submissions failed even when a raw POST looked fine from a terminal.
I rebuilt the contact backend in a dedicated repository, cloud-resume-api, and wired the website to the new endpoint.
Architecture
flowchart TD browser[Browser khansaiful.com] --> api["API Gateway POST /contact"] api --> lambda["Lambda cloud-resume-contact Python 3.13"] lambda --> ddb[(DynamoDB contact_messages)] lambda --> ses[Amazon SES optional]
What the frontend sends
src/js/contact-form.js posts JSON from the footer form:
nameemailmessage
A honeypot field (company) is ignored by bots that fill every input; humans never see it.
Backend behavior
- Validates required fields and length limits
- Stores every message in DynamoDB so nothing is lost
- Returns proper
Access-Control-Allow-Originfor khansaiful.com (and local Vite) - Attempts SES email when
CONTACT_TO_EMAILis configured and verified
Repository layout
Contact API source lives in cloud-resume-api:
lambda_function.py— request handlerinfra/deploy.ps1— DynamoDB, IAM, Lambda, HTTP API deploy helperREADME.md— endpoint contract and deploy notes
Optional: enable email delivery
SES starts in sandbox. Verify your destination (and preferred from-address), then redeploy the Lambda with:
$env:CONTACT_TO_EMAIL = "you@example.com"
$env:CONTACT_FROM_EMAIL = "noreply@khansaiful.com"
.\infra\deploy.ps1
Until then, submissions still succeed and are stored in DynamoDB (emailed: false in the API response).


