Project

Contact Form

Fill out the form and kick off the request flow end to end — client-side validation, a POST to API Gateway, and a Lambda function doing the work. Submit it and you'll land on a confirmation page; moments later, SES will send a confirmation email to your inbox.

Demo

Smoke Test

This form runs entirely in the browser and submits to AWS API Gateway with fetch(). No server code is needed in the static site.

Privacy notice: your email address is used only to test and validate this AWS architecture. No addresses are collected, stored, or used for marketing or any other purpose beyond this one-time test.

Architecture

How it works

This form is a real AWS project, not a mockup. Here's what actually happens between loading this page and an email landing in your inbox — click any step to jump to it, or let it play through on its own.

Users
Route 53
CloudFront
S3 Bucket
API Gateway
Lambda
SES
  1. 1

    Route 53 resolves the domain

    A user navigates to the contact form URL, triggering a DNS lookup. Route 53 resolves the domain name, returning the CloudFront distribution via an A record alias.

  2. 2

    CloudFront serves the static assets

    S3 returns the static assets (HTML, CSS, JavaScript) to CloudFront, which caches them at the edge and serves them to the browser.

  3. 3

    S3 stores the site's files

    The S3 bucket is the origin behind CloudFront — it holds the actual HTML, CSS, and JavaScript files that make up the page.

    • The bucket has all public access blocked — only CloudFront's OAC is permitted to read from it.
    • The browser parses and renders the HTML contact form using the JavaScript and CSS delivered from CloudFront. The JavaScript is responsible for intercepting the form submission and converting it into a JSON payload for the API call.
    • Preflight — before submitting the form, the browser automatically sends an HTTP OPTIONS request to API Gateway to confirm cross-origin requests are permitted. API Gateway responds with the appropriate CORS headers, and the browser proceeds with the actual POST request.
  4. 4

    API Gateway routes the request

    API Gateway receives the POST request and matches the defined HTTP API route configuration. It then proxies the request to Lambda.

  5. 5

    Lambda validates the submission

    Lambda parses the JSON request body and validates that all fields are present, correctly formatted, and within character limits.

    • Any invalid input returns a 400 response immediately, without engaging SES.
    • Lambda sanitizes inputs by stripping HTML-unsafe characters to prevent injection attacks.
    • AWS verifies that the function's IAM execution role includes the ses:SendEmail permission.
  6. 6

    SES sends the emails

    SES delivers two emails simultaneously: a notification to the admin, and a confirmation back to the visitor.

Use cases

Where this pattern fits

This serverless contact-form pattern isn't specific to this site — the same S3 + API Gateway + Lambda + SES combination covers a few common needs without standing up a traditional backend.

Static site contact forms

Any static site — built with a site generator, a plain HTML page, or a single-page app — that needs a working contact form without a backend server to maintain.

Lightweight lead capture

Landing pages that need to capture signups or interest before a full CRM integration is worth the setup cost.

Feedback & support intake

Early-stage products that need a simple way to collect feedback or support requests before a full helpdesk platform makes sense.

Pricing

What this actually costs

Every piece of this architecture is pay-per-use — there's no server sitting idle, so cost scales with traffic. These are estimates based on public AWS list pricing, not a guarantee; actual cost depends on your usage and configuration.

Low volume

~$0/mo

Up to around 1,000 submissions a month — comfortably inside the AWS Free Tier for Lambda, API Gateway, and SES.

Moderate volume

~$1–3/mo

Roughly 10,000–50,000 submissions a month — mostly SES sending costs once the free tier is used up.

High volume

~$10–20/mo

500,000+ submissions a month — Lambda and API Gateway costs start to add up alongside SES.

What drives the cost

  • Lambda — billed per request and per millisecond of compute time; this function runs in well under 100ms.
  • API Gateway — billed per million requests, using the cheaper HTTP API type rather than REST API.
  • SES — billed per 1,000 emails sent; two go out per submission, an admin notice and a visitor confirmation.
  • S3 — a few cents a month at most; static hosting for a small site is close to free.

Design decisions

Why I built it this way

Every architecture involves tradeoffs. Here's the reasoning behind some of the choices in this project — including a couple I only landed on after real deployment testing proved the original plan wrong.

A one-field "smoke test" instead of a full contact form

Early versions collected a name, subject, and message. Stripped down to just an email address on purpose — a demo visitor would just be typing placeholder text into those fields anyway. The point of this project is proving the architecture actually works end to end, not collecting real inquiries.

Two separate emails instead of one

The first working version sent a single email to me, with the visitor's address set as reply-to. That only proves half the point — a visitor should see their own confirmation too, not just trust that something happened on my end. Now SES sends two: a notification to me, and a separate confirmation back to whoever submitted the form.

Server-side validation, even though the browser already validates

The form checks required fields and email format before it ever submits. The Lambda checks all of it again anyway, because none of that client-side JavaScript is enforced — anyone can POST directly to the API and skip the browser entirely. The real validation is the one nothing can bypass.

A honeypot field instead of a CAPTCHA

One extra input, hidden from real visitors with CSS and never announced to screen readers, sits in the form. Bots that fill in every field trip it; the Lambda silently accepts the submission without sending mail. No third-party CAPTCHA service, no added friction for anyone actually testing the form.

CORS enforced at two layers, learned the hard way

Both API Gateway's own CORS configuration and the Lambda's own response headers check the request's origin — belt and suspenders. That redundancy isn't theoretical: getting this project working end to end surfaced a misconfigured function handler, a missing environment variable, and a failing preflight check, each one hidden behind the last. Real deployment testing found all three; none of them would have shown up from reading the code alone.

HTML-formatted emails instead of plain text

It would have been faster to leave both emails as plain text. A poorly formatted confirmation email undercuts the whole point of the site, though — if the goal is showing this is real, working infrastructure, the email a visitor actually receives should look like it came from something real.

A config.js scoped to this project alone

Rather than one shared config file for the whole site, this project keeps its own, holding only what it needs. As more projects get added here, a single growing config file would start implying a coupling between unrelated backends that doesn't actually exist.