Build an Interactive Product Demo

Build a live, S3-hosted demo page that calls your real serverless API.

Introduction

30 Second Summary

When you're selling a technical product, the most convincing thing you can do is show it working live. No slides, no hand-waving. Just a real demo that proves your system does what you say it does.

In this project, you will build a single-page interactive product demo that calls your real serverless API and showcases the full pipeline visually. By the end, you'll have a polished interview artifact that proves you can build AND present technical solutions.

What You'll Build

A medspa-themed lead capture demo called "Glow Medical Spa" that submits form data to your real LeadIntakeAPI, displays live API responses, and shows a visual pipeline status of what happened behind the scenes.

By the end of this project, you'll have:

  • CORS enabled on your API Gateway so browsers can call your API directly.
  • A complete web page built from scratch with HTML, CSS, and JavaScript in a single file that connects to a live backend.
  • A static website hosted on S3 accessible from any browser via a public URL.
  • Secret Mission: Add a Demo Mode toggle with pre-filled scenario buttons and a CSS architecture diagram for scripted interview demos.

Are there any prerequisites?

Yes. You need to have completed Project 1 (Build a Serverless Lead Intake API) with your LeadIntakeAPI deployed and working. You also need Project 2 (Secure Your API with Authentication) completed so you have an API key configured.

You should also have prior experience hosting a static website on Amazon S3.

Enable CORS on Your API

Your demo page will run in a browser and call your LeadIntakeAPI directly. Browsers enforce a security policy called CORS that blocks web pages from making requests to a different domain unless the server explicitly allows it.

Right now, your API has no CORS configuration. A browser trying to call it would be blocked immediately. In this step, you will update your Lambda function to include CORS headers in every response, then enable CORS on API Gateway so the preflight check passes too.

In this step, get ready to:
  • Update your Lambda function to return CORS headers on all responses.
  • Enable CORS on the /leads resource in API Gateway.
  • Redeploy the API and verify the CORS headers with a curl test.
Update your Lambda function to return CORS headers

Your LeadIntakeFunction currently returns only a Content-Type header in its responses. Because your API uses Lambda proxy integration, the Lambda function itself must include the CORS headers. API Gateway will not add them automatically for proxy integrations.

  • Sign in to the AWS Management Console.
  • Open the Lambda console by searching for Lambda in the top search bar and selecting it.
  • On the Functions page, click LeadIntakeFunction to open it.
  • Select the Code tab to open the built-in code editor.

You need to find every place in your code where a response is returned (the 200 success response, the 400 validation error response, and the 500 server error response). In each one, you will update the headers dictionary.

  • In the code editor, find your existing headers block that currently looks like this:
"headers": {
    "Content-Type": "application/json"
}
  • Replace it with the following updated headers block that includes CORS headers:
"headers": {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Content-Type,x-api-key",
    "Access-Control-Allow-Methods": "POST,OPTIONS"
}

What do these headers do?

  • Access-Control-Allow-Origin: * tells the browser that any domain is allowed to call this API.
  • Access-Control-Allow-Headers: Content-Type,x-api-key permits the browser to send your API key header along with the request.
  • Access-Control-Allow-Methods: POST,OPTIONS specifies which HTTP methods the browser is allowed to use.
  • Repeat this replacement for every return statement in your function (200, 400, and 500 responses). Each one needs the same four headers.
  • Click Deploy to save and deploy the updated function code.

You should see a green banner confirming the function was updated successfully.

Don't see a Deploy button?

Make sure you are on the Code tab and have made at least one edit to the code. The Deploy button appears in the code editor area above your file.

Help me find the Deploy button

Enable CORS on the /leads resource in API Gateway

Updating the Lambda handles actual responses, but browsers also send a preflight OPTIONS request before the real call. You need to enable CORS on the API Gateway resource so it knows how to respond to that preflight.

  • Open the API Gateway console by searching for API Gateway in the top search bar and selecting it.
  • Click LeadIntakeAPI in the APIs list.
  • In the left sidebar under Resources, click the /leads resource to select it.
  • In the Resource details section on the right, choose Enable CORS.
  • For Access-Control-Allow-Headers, clear the default value and enter Content-Type,x-api-key.
  • For Access-Control-Allow-Origin, enter *.
  • Confirm that both POST and OPTIONS methods are checked.
  • Click Save.

What is a preflight request?

Before sending a POST with custom headers, browsers first send an OPTIONS request to ask the server what is allowed. This is the preflight check. API Gateway's Enable CORS feature automatically creates an OPTIONS method that responds with the correct headers you configured.

Redeploy the API and test with curl

After enabling CORS, you must redeploy the API for the changes to take effect.

  • In the Resources pane, click Deploy API.
  • For Stage, select dev from the dropdown.
  • Click Deploy.

Now test that the CORS preflight is working by sending an OPTIONS request from your terminal.

  • Open your terminal and run the following curl command to send an OPTIONS preflight request:
curl -v -X OPTIONS https://[[API_ID="your-api-id"]].execute-api.[[AWS_REGION="your-region"]].amazonaws.com/dev/leads

What does this command do?

The -v flag shows verbose output including all response headers. The -X OPTIONS sends an OPTIONS request, which is exactly what a browser sends as the preflight check before calling your API.

Replace the API ID and region with the values from your own API Gateway invoke URL. You can find this in the Stages section of your API under Stage details.

  • In the response output, look for these three headers:
  • Access-Control-Allow-Origin should show *.
  • Access-Control-Allow-Headers should include Content-Type,x-api-key.
  • Access-Control-Allow-Methods should include POST and OPTIONS.

✔️ I see all three CORS headers

Your CORS configuration is working. The browser will now be able to call your API from any domain without being blocked.

ⓧ I don't see the CORS headers

  • Double check that you redeployed the API to the dev stage after enabling CORS. Changes do not take effect until you redeploy.
  • Verify you selected the /leads resource (not the root /) when you enabled CORS.
  • Confirm your curl URL ends with /dev/leads and uses your correct API ID and region.

Help me debug my CORS configuration

Your API is now ready to accept cross-origin requests from any browser. Next up, you will build the demo page that actually makes those calls.

Build the Demo Page

Your API is now ready to accept requests from a browser. Time to build the front-end that will actually call it.

In this step, you will create a single index.html file that contains all the HTML structure, CSS styling, and placeholder JavaScript for your medspa lead capture demo.

In this step, get ready to:
  • Create the HTML structure with a branded header, lead capture form, and status panels.
  • Add embedded CSS styling for a polished, professional look.
  • Verify the static page renders correctly in your browser.
Create the HTML structure

Your demo page needs a header to brand it, a form to collect lead information, and panels to show what happens behind the scenes after submission.

Why a single file?

Keeping HTML, CSS, and JavaScript in one file eliminates the complexity of linking separate files and debugging broken references.

Everything is visible in one place, which makes it easier to understand how the three languages work together.

  • Open your text editor (TextEdit, VS Code, or any editor you prefer).
  • Create a new file and save it as index.html on your Desktop.
  • Paste the following HTML structure into the file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Glow Medical Spa - Lead Capture Demo</title>
    <style>
        /* CSS styling will go here */
    </style>
</head>
<body>
    <div class="container">
        <!-- Branded header -->
        <div class="header">
            <h1>Glow Medical Spa</h1>
            <p>Lead Capture System - Interactive Product Demo</p>
        </div>

        <!-- Lead capture form -->
        <div class="form-card">
            <h2>New Lead Inquiry</h2>
            <form id="lead-form">
                <div class="form-row">
                    <div class="form-group">
                        <label for="first-name">First Name</label>
                        <input type="text" id="first-name" placeholder="Jane" required>
                    </div>
                    <div class="form-group">
                        <label for="last-name">Last Name</label>
                        <input type="text" id="last-name" placeholder="Smith" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="email">Email</label>
                        <input type="email" id="email" placeholder="jane@example.com" required>
                    </div>
                    <div class="form-group">
                        <label for="phone">Phone</label>
                        <input type="tel" id="phone" placeholder="555-123-4567" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="service">Service Interest</label>
                        <select id="service" required>
                            <option value="">Select a service...</option>
                            <option value="Botox">Botox</option>
                            <option value="Dermal Fillers">Dermal Fillers</option>
                            <option value="Chemical Peel">Chemical Peel</option>
                            <option value="Laser Hair Removal">Laser Hair Removal</option>
                            <option value="Microneedling">Microneedling</option>
                            <option value="Consultation">Consultation</option>
                        </select>
                    </div>
                </div>
                <button type="submit" class="submit-btn" id="submit-btn">Submit Lead</button>
                <p class="error-message" id="error-message"></p>
            </form>
        </div>

        <!-- Pipeline status panel -->
        <div class="pipeline-card" id="pipeline-card">
            <h2>Pipeline Status</h2>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-1-icon">-</div>
                <span class="step-label">API Request Received</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-2-icon">-</div>
                <span class="step-label">Validation Passed</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-3-icon">-</div>
                <span class="step-label">Saved to DynamoDB</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-4-icon">-</div>
                <span class="step-label">Lead ID Generated</span>
            </div>
        </div>

        <!-- API response display -->
        <div class="response-card" id="response-card">
            <h2>API Response</h2>
            <div class="response-body" id="response-body"></div>
        </div>
    </div>

    <script>
        // Configuration - Replace these with your actual values
        const API_URL = "https://your-api-id.execute-api.your-region.amazonaws.com/dev/leads";
        const API_KEY = "your-api-key-here";
    </script>
</body>
</html>

What does this code do?

  • The header div displays the spa brand name and a subtitle describing the demo.
  • The form-card contains the lead capture form with five fields: first name, last name, email, phone, and a service interest dropdown.
  • The pipeline-card has four status steps that will animate from pending to success when the API responds. It starts hidden.
  • The response-card will show the raw JSON response from your API. It also starts hidden.
  • The <script> block at the bottom defines placeholder constants for your API URL and API key. You will replace these with your real values in the next step.
  • Save index.html.
  • You won't see a styled page yet. The next chunk adds the CSS that makes everything look polished.
Add the CSS styling

CSS controls how your HTML elements look on screen. You will add a complete stylesheet that gives the page a professional, calming design with card-based layout and teal branding.

  • In index.html, find the line that says /* CSS styling will go here */ inside the <style> block.
  • Replace that comment with the following CSS:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: linear-gradient(135deg, #f0fdfa 0%, #ffffff 100%);
    min-height: 100vh;
    padding: 40px 20px;
}

.container {
    max-width: 800px;
    margin: 0 auto;
}

.header {
    text-align: center;
    margin-bottom: 40px;
}

.header h1 {
    color: #0f766e;
    font-size: 28px;
    margin-bottom: 8px;
}

.header p {
    color: #6b7280;
    font-size: 14px;
}

.form-card {
    background: white;
    border-radius: 12px;
    padding: 32px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
    margin-bottom: 24px;
}

.form-card h2 {
    color: #1f2937;
    font-size: 20px;
    margin-bottom: 24px;
}

.form-row {
    display: flex;
    gap: 16px;
    margin-bottom: 16px;
}

.form-group {
    flex: 1;
    display: flex;
    flex-direction: column;
}

label {
    font-size: 13px;
    font-weight: 500;
    color: #374151;
    margin-bottom: 6px;
}

input, select {
    padding: 10px 12px;
    border: 1px solid #d1d5db;
    border-radius: 8px;
    font-size: 14px;
    transition: border-color 0.2s;
}

input:focus, select:focus {
    outline: none;
    border-color: #0f766e;
}

.submit-btn {
    width: 100%;
    padding: 12px;
    background: #0f766e;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 16px;
    font-weight: 500;
    cursor: pointer;
    margin-top: 8px;
    transition: background 0.2s;
}

.submit-btn:hover {
    background: #0d9488;
}

.submit-btn:disabled {
    background: #9ca3af;
    cursor: not-allowed;
}

.error-message {
    color: #dc2626;
    font-size: 13px;
    margin-top: 8px;
    display: none;
}

.error-message.visible {
    display: block;
}

.pipeline-card {
    background: white;
    border-radius: 12px;
    padding: 32px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
    margin-bottom: 24px;
    display: none;
}

.pipeline-card.visible {
    display: block;
}

.pipeline-card h2 {
    color: #1f2937;
    font-size: 20px;
    margin-bottom: 20px;
}

.pipeline-step {
    display: flex;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid #f3f4f6;
}

.pipeline-step:last-child {
    border-bottom: none;
}

.step-icon {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
    font-size: 14px;
}

.step-icon.pending {
    background: #f3f4f6;
    color: #9ca3af;
}

.step-icon.success {
    background: #d1fae5;
    color: #059669;
}

.step-icon.error {
    background: #fee2e2;
    color: #dc2626;
}

.step-label {
    font-size: 14px;
    color: #374151;
}

.response-card {
    background: white;
    border-radius: 12px;
    padding: 32px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
    margin-bottom: 24px;
    display: none;
}

.response-card.visible {
    display: block;
}

.response-card h2 {
    color: #1f2937;
    font-size: 20px;
    margin-bottom: 16px;
}

.response-body {
    background: #f9fafb;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: 16px;
    font-family: monospace;
    font-size: 13px;
    white-space: pre-wrap;
    color: #1f2937;
}

What does this CSS do?

  • The * reset removes default browser spacing so every element starts from a clean slate.
  • The body uses a soft teal-to-white gradient background that gives the page a calming, medical spa feel.
  • Each content section (.form-card, .pipeline-card, .response-card) is a white card with rounded corners and a subtle shadow.
  • The .form-row class uses flexbox to place fields side-by-side in two-column rows.
  • The pipeline and response panels use display: none by default and only appear when JavaScript adds the visible class.
  • Save index.html.
Verify the static page
  • Open index.html in your browser by double-clicking the file on your Desktop.
  • Confirm you see the teal "Glow Medical Spa" heading centered at the top.
  • Check that the form displays all five fields (First Name, Last Name, Email, Phone, Service Interest) and a teal Submit Lead button.
  • Verify the pipeline status and API response panels are NOT visible on the page. They will only appear after a form submission.

What should I see?

You should see a clean page with a soft gradient background. The header shows "Glow Medical Spa" in teal. Below it, a white card contains the form with placeholder text in each field. The submit button is solid teal.

Nothing appears below the form card. The pipeline and response sections are hidden until JavaScript makes them visible.

Page looks unstyled or broken?

  • Make sure your CSS is inside the <style> tags in the <head> section. CSS outside these tags will not be applied.
  • Check that you deleted the /* CSS styling will go here */ comment and replaced it with the CSS code. Leaving the comment is fine, but the CSS must be pasted alongside it.
  • If you are using TextEdit on macOS, make sure it is in plain text mode. Go to Format and select Make Plain Text before saving.

Still stuck? Help me debug why my index.html page looks unstyled in the browser.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file as it should look at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Glow Medical Spa - Lead Capture Demo</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: linear-gradient(135deg, #f0fdfa 0%, #ffffff 100%);
            min-height: 100vh;
            padding: 40px 20px;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
        }

        .header {
            text-align: center;
            margin-bottom: 40px;
        }

        .header h1 {
            color: #0f766e;
            font-size: 28px;
            margin-bottom: 8px;
        }

        .header p {
            color: #6b7280;
            font-size: 14px;
        }

        .form-card {
            background: white;
            border-radius: 12px;
            padding: 32px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            margin-bottom: 24px;
        }

        .form-card h2 {
            color: #1f2937;
            font-size: 20px;
            margin-bottom: 24px;
        }

        .form-row {
            display: flex;
            gap: 16px;
            margin-bottom: 16px;
        }

        .form-group {
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        label {
            font-size: 13px;
            font-weight: 500;
            color: #374151;
            margin-bottom: 6px;
        }

        input, select {
            padding: 10px 12px;
            border: 1px solid #d1d5db;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.2s;
        }

        input:focus, select:focus {
            outline: none;
            border-color: #0f766e;
        }

        .submit-btn {
            width: 100%;
            padding: 12px;
            background: #0f766e;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            margin-top: 8px;
            transition: background 0.2s;
        }

        .submit-btn:hover {
            background: #0d9488;
        }

        .submit-btn:disabled {
            background: #9ca3af;
            cursor: not-allowed;
        }

        .error-message {
            color: #dc2626;
            font-size: 13px;
            margin-top: 8px;
            display: none;
        }

        .error-message.visible {
            display: block;
        }

        .pipeline-card {
            background: white;
            border-radius: 12px;
            padding: 32px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            margin-bottom: 24px;
            display: none;
        }

        .pipeline-card.visible {
            display: block;
        }

        .pipeline-card h2 {
            color: #1f2937;
            font-size: 20px;
            margin-bottom: 20px;
        }

        .pipeline-step {
            display: flex;
            align-items: center;
            padding: 12px 0;
            border-bottom: 1px solid #f3f4f6;
        }

        .pipeline-step:last-child {
            border-bottom: none;
        }

        .step-icon {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 12px;
            font-size: 14px;
        }

        .step-icon.pending {
            background: #f3f4f6;
            color: #9ca3af;
        }

        .step-icon.success {
            background: #d1fae5;
            color: #059669;
        }

        .step-icon.error {
            background: #fee2e2;
            color: #dc2626;
        }

        .step-label {
            font-size: 14px;
            color: #374151;
        }

        .response-card {
            background: white;
            border-radius: 12px;
            padding: 32px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            margin-bottom: 24px;
            display: none;
        }

        .response-card.visible {
            display: block;
        }

        .response-card h2 {
            color: #1f2937;
            font-size: 20px;
            margin-bottom: 16px;
        }

        .response-body {
            background: #f9fafb;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            padding: 16px;
            font-family: monospace;
            font-size: 13px;
            white-space: pre-wrap;
            color: #1f2937;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Glow Medical Spa</h1>
            <p>Lead Capture System - Interactive Product Demo</p>
        </div>

        <div class="form-card">
            <h2>New Lead Inquiry</h2>
            <form id="lead-form">
                <div class="form-row">
                    <div class="form-group">
                        <label for="first-name">First Name</label>
                        <input type="text" id="first-name" placeholder="Jane" required>
                    </div>
                    <div class="form-group">
                        <label for="last-name">Last Name</label>
                        <input type="text" id="last-name" placeholder="Smith" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="email">Email</label>
                        <input type="email" id="email" placeholder="jane@example.com" required>
                    </div>
                    <div class="form-group">
                        <label for="phone">Phone</label>
                        <input type="tel" id="phone" placeholder="555-123-4567" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="service">Service Interest</label>
                        <select id="service" required>
                            <option value="">Select a service...</option>
                            <option value="Botox">Botox</option>
                            <option value="Dermal Fillers">Dermal Fillers</option>
                            <option value="Chemical Peel">Chemical Peel</option>
                            <option value="Laser Hair Removal">Laser Hair Removal</option>
                            <option value="Microneedling">Microneedling</option>
                            <option value="Consultation">Consultation</option>
                        </select>
                    </div>
                </div>
                <button type="submit" class="submit-btn" id="submit-btn">Submit Lead</button>
                <p class="error-message" id="error-message"></p>
            </form>
        </div>

        <div class="pipeline-card" id="pipeline-card">
            <h2>Pipeline Status</h2>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-1-icon">-</div>
                <span class="step-label">API Request Received</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-2-icon">-</div>
                <span class="step-label">Validation Passed</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-3-icon">-</div>
                <span class="step-label">Saved to DynamoDB</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-4-icon">-</div>
                <span class="step-label">Lead ID Generated</span>
            </div>
        </div>

        <div class="response-card" id="response-card">
            <h2>API Response</h2>
            <div class="response-body" id="response-body"></div>
        </div>
    </div>

    <script>
        // Configuration - Replace these with your actual values
        const API_URL = "https://your-api-id.execute-api.your-region.amazonaws.com/dev/leads";
        const API_KEY = "your-api-key-here";
    </script>
</body>
</html>

Your demo page looks polished, but it does not do anything yet. Next up, you will wire the form to your real API so clicking Submit Lead sends data to your serverless backend and animates the pipeline in real time.

Connect the Form to Your API

Your demo page looks great, but right now it's purely visual. Clicking Submit Lead does nothing because there is no JavaScript logic to handle the form.

In this step, you will wire up the form to your real LeadIntakeAPI using the browser's built-in fetch() API. When a prospect submits their details, the page will call your API, animate the pipeline status steps, and display the live response.

In this step, get ready to:
  • Add DOM element references and a form submit event listener.
  • Write the async submitLead() function that calls your API with fetch().
  • Test a successful submission and a validation error in your browser.
Add the DOM element references and form handler

Before you can respond to form submissions, JavaScript needs references to the HTML elements it will update. You also need an event listener that intercepts the default form behavior and runs your custom logic instead.

  • In index.html, find the <script> block at the bottom of the file. You should see the two constants you added earlier (API_URL and API_KEY).
  • Directly below the API_KEY line, add the following DOM references and event listener:
// DOM elements
const form = document.getElementById("lead-form");
const submitBtn = document.getElementById("submit-btn");
const errorMessage = document.getElementById("error-message");
const pipelineCard = document.getElementById("pipeline-card");
const responseCard = document.getElementById("response-card");
const responseBody = document.getElementById("response-body");

// Form submit handler
form.addEventListener("submit", function(event) {
    event.preventDefault();
    submitLead();
});

What does this code do?

The six const lines grab references to elements you created in the HTML. Caching them in variables avoids searching the DOM every time you need to update one.

The event listener intercepts the form's native submit behavior (which would reload the page) by calling event.preventDefault(). It then calls submitLead(), the function you will write next.

Write the submitLead() function with fetch()

This is the core of your demo. The submitLead() function collects form values, sends them to your API, and updates the pipeline status based on the response.

  • Below the event listener you just added, paste this async function:
async function submitLead() {
    // Collect form values
    const firstName = document.getElementById("first-name").value.trim();
    const lastName = document.getElementById("last-name").value.trim();
    const email = document.getElementById("email").value.trim();
    const phone = document.getElementById("phone").value.trim();
    const service = document.getElementById("service").value;

    // Hide previous errors and show pipeline
    errorMessage.classList.remove("visible");
    pipelineCard.classList.add("visible");
    responseCard.classList.remove("visible");
    resetPipelineSteps();

    // Disable button while request is in flight
    submitBtn.disabled = true;
    submitBtn.textContent = "Submitting...";

    // Build request body - combine first and last name
    const body = {
        name: firstName + " " + lastName,
        email: email,
        phone: phone,
        service_interest: service
    };

    try {
        // Step 1: API request sent
        updateStep("step-1-icon", "success");

        const response = await fetch(API_URL, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-api-key": API_KEY
            },
            body: JSON.stringify(body)
        });

        const data = await response.json();

        if (response.ok) {
            // Step 2-4: Pipeline success with staggered timing
            updateStep("step-2-icon", "success");
            setTimeout(function() { updateStep("step-3-icon", "success"); }, 300);
            setTimeout(function() { updateStep("step-4-icon", "success"); }, 600);
            setTimeout(function() {
                responseCard.classList.add("visible");
                responseBody.textContent = JSON.stringify(data, null, 2);
            }, 800);
        } else {
            // Validation failed - show error from API
            updateStep("step-2-icon", "error");
            errorMessage.textContent = data.errors ? data.errors.join(", ") : data.error || "Request failed";
            errorMessage.classList.add("visible");
            responseCard.classList.add("visible");
            responseBody.textContent = JSON.stringify(data, null, 2);
        }
    } catch (err) {
        // Network error - API unreachable
        updateStep("step-1-icon", "error");
        errorMessage.textContent = "Network error: " + err.message;
        errorMessage.classList.add("visible");
    }

    // Re-enable button
    submitBtn.disabled = false;
    submitBtn.textContent = "Submit Lead";
}

What does this code do?

  • It collects the five form field values and combines firstName + lastName into a single name field, which is what your LeadIntakeAPI expects.
  • It calls fetch() with the POST method, passing your x-api-key header for authentication and a JSON body.
  • On a successful response (response.ok), it animates pipeline steps to green checkmarks using setTimeout for a staggered visual effect.
  • On a 400 error, it marks the validation step as failed and displays the API's error messages in the form.
  • The catch block handles network-level failures (API unreachable, CORS blocked).

Now add the two helper functions that submitLead() relies on. These manage the pipeline step icons.

  • Below the closing brace of submitLead(), paste these two functions:
function updateStep(stepId, status) {
    // Change the icon appearance based on success or error
    const icon = document.getElementById(stepId);
    icon.className = "step-icon " + status;
    icon.textContent = status === "success" ? "\u2713" : "\u2717";
}

function resetPipelineSteps() {
    // Reset all pipeline icons to pending state
    var steps = ["step-1-icon", "step-2-icon", "step-3-icon", "step-4-icon"];
    for (var i = 0; i < steps.length; i++) {
        var icon = document.getElementById(steps[i]);
        icon.className = "step-icon pending";
        icon.textContent = "-";
    }
}

What do these helpers do?

  • updateStep() takes a step ID and a status (success or error). It swaps the CSS class so the icon turns green or red, and changes the text to a checkmark or cross.
  • resetPipelineSteps() loops through all four pipeline step icons and resets them to the gray "pending" state before each new submission.
  • Save index.html.

Seeing a syntax error in the console?

  • Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for red error messages.
  • Check that your new code is inside the existing <script> block, not outside it.
  • Make sure every opening brace { has a matching closing brace }.

Help me find the syntax error in my JavaScript code.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html before moving on to testing.

ⓧ I'd like to double check the full code

Here is the complete index.html file at this point. Compare yours against it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Glow Medical Spa - Lead Capture Demo</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: linear-gradient(135deg, #f0fdfa 0%, #ffffff 100%);
            min-height: 100vh;
            padding: 40px 20px;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
        }

        .header {
            text-align: center;
            margin-bottom: 40px;
        }

        .header h1 {
            color: #0f766e;
            font-size: 28px;
            margin-bottom: 8px;
        }

        .header p {
            color: #6b7280;
            font-size: 14px;
        }

        .form-card {
            background: white;
            border-radius: 12px;
            padding: 32px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            margin-bottom: 24px;
        }

        .form-card h2 {
            color: #1f2937;
            font-size: 20px;
            margin-bottom: 24px;
        }

        .form-row {
            display: flex;
            gap: 16px;
            margin-bottom: 16px;
        }

        .form-group {
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        label {
            font-size: 13px;
            font-weight: 500;
            color: #374151;
            margin-bottom: 6px;
        }

        input, select {
            padding: 10px 12px;
            border: 1px solid #d1d5db;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.2s;
        }

        input:focus, select:focus {
            outline: none;
            border-color: #0f766e;
        }

        .submit-btn {
            width: 100%;
            padding: 12px;
            background: #0f766e;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            margin-top: 8px;
            transition: background 0.2s;
        }

        .submit-btn:hover {
            background: #0d9488;
        }

        .submit-btn:disabled {
            background: #9ca3af;
            cursor: not-allowed;
        }

        .error-message {
            color: #dc2626;
            font-size: 13px;
            margin-top: 8px;
            display: none;
        }

        .error-message.visible {
            display: block;
        }

        .pipeline-card {
            background: white;
            border-radius: 12px;
            padding: 32px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            margin-bottom: 24px;
            display: none;
        }

        .pipeline-card.visible {
            display: block;
        }

        .pipeline-card h2 {
            color: #1f2937;
            font-size: 20px;
            margin-bottom: 20px;
        }

        .pipeline-step {
            display: flex;
            align-items: center;
            padding: 12px 0;
            border-bottom: 1px solid #f3f4f6;
        }

        .pipeline-step:last-child {
            border-bottom: none;
        }

        .step-icon {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 12px;
            font-size: 14px;
        }

        .step-icon.pending {
            background: #f3f4f6;
            color: #9ca3af;
        }

        .step-icon.success {
            background: #d1fae5;
            color: #059669;
        }

        .step-icon.error {
            background: #fee2e2;
            color: #dc2626;
        }

        .step-label {
            font-size: 14px;
            color: #374151;
        }

        .response-card {
            background: white;
            border-radius: 12px;
            padding: 32px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            margin-bottom: 24px;
            display: none;
        }

        .response-card.visible {
            display: block;
        }

        .response-card h2 {
            color: #1f2937;
            font-size: 20px;
            margin-bottom: 16px;
        }

        .response-body {
            background: #f9fafb;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            padding: 16px;
            font-family: monospace;
            font-size: 13px;
            white-space: pre-wrap;
            color: #1f2937;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Glow Medical Spa</h1>
            <p>Lead Capture System - Interactive Product Demo</p>
        </div>

        <div class="form-card">
            <h2>New Lead Inquiry</h2>
            <form id="lead-form">
                <div class="form-row">
                    <div class="form-group">
                        <label for="first-name">First Name</label>
                        <input type="text" id="first-name" placeholder="Jane" required>
                    </div>
                    <div class="form-group">
                        <label for="last-name">Last Name</label>
                        <input type="text" id="last-name" placeholder="Smith" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="email">Email</label>
                        <input type="email" id="email" placeholder="jane@example.com" required>
                    </div>
                    <div class="form-group">
                        <label for="phone">Phone</label>
                        <input type="tel" id="phone" placeholder="555-123-4567" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="service">Service Interest</label>
                        <select id="service" required>
                            <option value="">Select a service...</option>
                            <option value="Botox">Botox</option>
                            <option value="Dermal Fillers">Dermal Fillers</option>
                            <option value="Chemical Peel">Chemical Peel</option>
                            <option value="Laser Hair Removal">Laser Hair Removal</option>
                            <option value="Microneedling">Microneedling</option>
                            <option value="Consultation">Consultation</option>
                        </select>
                    </div>
                </div>
                <button type="submit" class="submit-btn" id="submit-btn">Submit Lead</button>
                <p class="error-message" id="error-message"></p>
            </form>
        </div>

        <div class="pipeline-card" id="pipeline-card">
            <h2>Pipeline Status</h2>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-1-icon">-</div>
                <span class="step-label">API Request Received</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-2-icon">-</div>
                <span class="step-label">Validation Passed</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-3-icon">-</div>
                <span class="step-label">Saved to DynamoDB</span>
            </div>
            <div class="pipeline-step">
                <div class="step-icon pending" id="step-4-icon">-</div>
                <span class="step-label">Lead ID Generated</span>
            </div>
        </div>

        <div class="response-card" id="response-card">
            <h2>API Response</h2>
            <div class="response-body" id="response-body"></div>
        </div>
    </div>

    <script>
        // Configuration - Replace these with your actual values
        const API_URL = "https://your-api-id.execute-api.your-region.amazonaws.com/dev/leads";
        const API_KEY = "your-api-key-here";

        // DOM elements
        const form = document.getElementById("lead-form");
        const submitBtn = document.getElementById("submit-btn");
        const errorMessage = document.getElementById("error-message");
        const pipelineCard = document.getElementById("pipeline-card");
        const responseCard = document.getElementById("response-card");
        const responseBody = document.getElementById("response-body");

        // Form submit handler
        form.addEventListener("submit", function(event) {
            event.preventDefault();
            submitLead();
        });

        async function submitLead() {
            const firstName = document.getElementById("first-name").value.trim();
            const lastName = document.getElementById("last-name").value.trim();
            const email = document.getElementById("email").value.trim();
            const phone = document.getElementById("phone").value.trim();
            const service = document.getElementById("service").value;

            errorMessage.classList.remove("visible");
            pipelineCard.classList.add("visible");
            responseCard.classList.remove("visible");
            resetPipelineSteps();

            submitBtn.disabled = true;
            submitBtn.textContent = "Submitting...";

            const body = {
                name: firstName + " " + lastName,
                email: email,
                phone: phone,
                service_interest: service
            };

            try {
                updateStep("step-1-icon", "success");

                const response = await fetch(API_URL, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                        "x-api-key": API_KEY
                    },
                    body: JSON.stringify(body)
                });

                const data = await response.json();

                if (response.ok) {
                    updateStep("step-2-icon", "success");
                    setTimeout(function() { updateStep("step-3-icon", "success"); }, 300);
                    setTimeout(function() { updateStep("step-4-icon", "success"); }, 600);
                    setTimeout(function() {
                        responseCard.classList.add("visible");
                        responseBody.textContent = JSON.stringify(data, null, 2);
                    }, 800);
                } else {
                    updateStep("step-2-icon", "error");
                    errorMessage.textContent = data.errors ? data.errors.join(", ") : data.error || "Request failed";
                    errorMessage.classList.add("visible");
                    responseCard.classList.add("visible");
                    responseBody.textContent = JSON.stringify(data, null, 2);
                }
            } catch (err) {
                updateStep("step-1-icon", "error");
                errorMessage.textContent = "Network error: " + err.message;
                errorMessage.classList.add("visible");
            }

            submitBtn.disabled = false;
            submitBtn.textContent = "Submit Lead";
        }

        function updateStep(stepId, status) {
            const icon = document.getElementById(stepId);
            icon.className = "step-icon " + status;
            icon.textContent = status === "success" ? "\u2713" : "\u2717";
        }

        function resetPipelineSteps() {
            var steps = ["step-1-icon", "step-2-icon", "step-3-icon", "step-4-icon"];
            for (var i = 0; i < steps.length; i++) {
                var icon = document.getElementById(steps[i]);
                icon.className = "step-icon pending";
                icon.textContent = "-";
            }
        }
    </script>
</body>
</html>
Test the live connection

Your code is complete. Time to confirm the form actually talks to your real API and handles both success and failure gracefully.

  • Open index.html in your browser by double-clicking the file (or refresh if it is already open).
  • Fill in the form with test data: Jane for First Name, Smith for Last Name, jane@example.com for Email, 555-123-4567 for Phone, and select Botox from the Service Interest dropdown.
  • Click Submit Lead.

You should see the Pipeline Status panel appear with all four steps animating from gray to green checkmarks. After about a second, the API Response panel appears showing a JSON object with a message and lead_id from your real API.

  • Now test a validation error. Clear the Email field so it is empty, then click Submit Lead again.

The second pipeline step should turn red, and a red error message appears below the button showing the missing field error from your API.

Pipeline steps not animating?

  • Open your browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). If you see a CORS error, go back to Step 1 and verify your Lambda function returns CORS headers and that you redeployed the API.
  • If you see "Network error", check that API_URL contains your real API Gateway invoke URL ending in /dev/leads.
  • If the response says "Forbidden", confirm that API_KEY holds the correct x-api-key value from your API Gateway usage plan.

My form submission is failing with a CORS or network error. Help me debug it.

Your demo page is now fully functional. It captures leads, calls your real API, and visualizes the entire pipeline in real time. Next up, you will host this page on S3 so anyone can access it from a public URL.

Host on S3

Your demo page works locally. Now it is time to make it accessible from any browser, anywhere.

In this step, you will upload your index.html to an Amazon S3 bucket configured for static website hosting. This gives you a public URL you can share in interviews or send to stakeholders.

In this step, get ready to:
  • Create an S3 bucket and enable static website hosting.
  • Upload your demo page and verify the full pipeline works from a public URL.
Create the S3 bucket and enable static website hosting

Your demo page is a single HTML file with no server-side code. S3 static website hosting is designed for exactly this scenario.

  • Return to the AWS Management Console.
  • Search for S3 in the top search bar and open the S3 console.
  • Click Create bucket.
  • Enter a unique bucket name such as glow-medspa-demo-yourname (replace yourname with your own name to make it globally unique).
  • Leave the region set to your preferred region.
  • Scroll down to Block Public Access settings for this bucket.
  • Uncheck Block all public access (this unchecks all four options).
  • Check the acknowledgement box that appears confirming you understand these settings.
  • Scroll to the bottom and click Create bucket.

Why disable Block Public Access?

S3 blocks public access by default to protect sensitive data. Since your demo page is meant to be viewed by anyone with the URL, you need to explicitly allow public read access. This is the standard configuration for any static website hosted on S3.

Now enable static website hosting on your new bucket.

  • Click on your newly created bucket name to open it.
  • Click the Properties tab.
  • Scroll down to the Static website hosting section at the bottom of the page.
  • Click Edit.
  • Select Enable for Static website hosting.
  • In the Index document field, enter index.html.
  • Click Save changes.

Finally, add a bucket policy that allows anyone to read objects from your bucket.

  • Click the Permissions tab.
  • Scroll down to Bucket policy and click Edit.
  • Paste the following policy into the Bucket policy editor, replacing the bucket name with your own:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::[[BUCKET_NAME=\"glow-medspa-demo-yourname\"]]/*"
            ]
        }
    ]
}
  • Click Save changes.

What does this policy do?

This bucket policy grants s3:GetObject permission to everyone (Principal: *). It means any browser can download files from your bucket. The Resource line uses /* to apply this to all objects in the bucket.

Seeing "Policy has invalid resource"?

Confirm that the bucket name in the Resource field matches your actual bucket name exactly. A single typo or extra space will cause this error.

If you cannot save the policy at all, go back to the Permissions tab and confirm that all four Block Public Access checkboxes are unchecked.

Help me fix a bucket policy error in S3.

Upload index.html and test the live site

Your bucket is configured. Now upload your demo page and see it live.

  • Click the Objects tab in your bucket.
  • Click Upload.
  • Click Add files and select your index.html file from your computer.
  • Click Upload at the bottom of the page.
  • Wait for the success banner, then click Close.

Now grab your website endpoint URL.

  • Click the Properties tab.
  • Scroll down to the Static website hosting section.
  • Copy the Bucket website endpoint URL (it looks like http://glow-medspa-demo-yourname.s3-website-us-east-1.amazonaws.com).
  • Open this URL in a new browser tab.

Why HTTP instead of HTTPS?

S3 website endpoints do not support HTTPS. For a demo or learning project, HTTP is fine. In production, you would add Amazon CloudFront in front of the bucket to enable HTTPS.

Test the full pipeline from your live public URL.

  • Fill in the form with test data (e.g., first name Jane, last name Smith, email jane@example.com, phone 555-123-4567, service Botox).
  • Click Submit Lead.
  • Watch the pipeline status steps animate from pending to success with checkmarks.
  • Confirm the API Response panel shows a lead_id and the message Lead received successfully.

What should I see?

All four pipeline steps should show green checkmarks. The response panel displays the JSON your real API returned, including a lead_id that combines the email and timestamp. Your lead is now stored in DynamoDB.

  • Test the error case: submit the form with the email field left empty.
  • Confirm you see a red X on the Validation Passed step and an error message below the form.

Form submits but nothing happens?

Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for a CORS error in red text. If you see one, return to Step 1 and confirm your Lambda function returns the CORS headers on every response.

If you see a network error instead, double-check that the API_URL and API_KEY values in your index.html are correct. You may need to re-upload the file after fixing them.

Help me debug my S3-hosted demo page that won't connect to my API.

Secret mission

Add Demo Mode

Your demo works, but in an interview you'd fumble typing fake data into the form while explaining architecture. Add a Demo Mode toggle that reveals pre-filled scenario buttons and a live CSS architecture diagram that highlights active components as data flows through the system.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. All resources created in this project fall within the AWS Free Tier for 12 months of normal learning-level traffic, so there are no ongoing costs.

Resources you used:

  • S3 bucket (glow-medspa-demo-yourname) with static website hosting enabled.
  • CORS configuration (OPTIONS method) on the /leads resource in API Gateway.
  • Updated LeadIntakeFunction with CORS headers in Lambda.
  • Local index.html file on your computer.

Keep everything running

No action needed. Choose this if you plan to use your demo page for interview practice or want to keep building on it.

  • Your S3 bucket stays live at its website endpoint URL at no cost within the Free Tier (5 GB storage, 20,000 GET requests per month for 12 months).
  • Your Lambda function remains within the Free Tier (1 million requests per month).
  • The CORS configuration on API Gateway has no additional cost.
  • Your local index.html file stays on your computer for future edits.

Pause - I'll come back to this later

Take your demo page offline without deleting the bucket. This keeps everything intact so you can re-enable it later.

  • Open the S3 console at https://console.aws.amazon.com/s3/.
  • Select your bucket, then open the Properties tab.
  • Under Static website hosting, choose Edit.
  • Select Disable and choose Save changes.
  • Your demo page URL stops working, but your index.html file and bucket remain intact. Re-enable static website hosting when you want the page live again.

Delete - I don't want to use this again

Remove all project resources completely. Follow each step below.

Delete the S3 bucket:

  • Open the S3 console at https://console.aws.amazon.com/s3/.
  • In the left navigation pane, choose General purpose buckets.
  • Select the option next to your bucket name, then choose Empty.
  • Type permanently delete to confirm, then choose Empty.
  • Return to the bucket list, select your bucket again, then choose Delete.
  • Type your bucket name to confirm, then choose Delete bucket.

Remove CORS from API Gateway:

  • Open the API Gateway console at https://console.aws.amazon.com/apigateway.
  • Select LeadIntakeAPI, then choose Resources in the left navigation.
  • Select the /leads resource, then select the OPTIONS method.
  • Choose Delete method and confirm the deletion.
  • Deploy the API to the dev stage to apply the change.

Delete the local file:

  • Delete the index.html file from wherever you saved it on your computer (Desktop, Documents, or other location).

What about the Lambda CORS headers?

The CORS headers you added to LeadIntakeFunction do not cost anything extra and do not affect the function's normal operation. You can leave them in place safely. They only matter when a browser makes the request.

Nice Work!

Nice Work!

Nice work! You just built a live, interactive product demo that calls your real serverless backend and showcases the full pipeline visually.

You've learned how to:

  • Enabled CORS on API Gateway and updated your Lambda function to return cross-origin headers, allowing browser-based applications to call your API.
  • Built a complete web page from scratch using HTML (structure), CSS (styling), and JavaScript (behavior) in a single file, using the fetch() API to make HTTP requests with custom headers and JSON body handling.
  • Hosted a static website on S3 with public access, creating a polished medspa-themed product demo accessible from any browser that proves you can build AND present technical solutions.
  • Secret Mission: Added Demo Mode with pre-filled scenario buttons and a CSS architecture diagram for scripted interview demos.

Ready to quiz yourself?