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:

  • name
  • email
  • message

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-Origin for khansaiful.com (and local Vite)
  • Attempts SES email when CONTACT_TO_EMAIL is configured and verified

Repository layout

Contact API source lives in cloud-resume-api:

  • lambda_function.py — request handler
  • infra/deploy.ps1 — DynamoDB, IAM, Lambda, HTTP API deploy helper
  • README.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).

Try the form on this page →