New: Boardroom MCP Engine!

Ready to put this into action?

Get the complete AI Integration Playbook β€” Practical AI implementation guide β€” prompt engineering, workflow automation, and ROI frameworks.

Article 107 Β· Part 11

Build a Small Website or Application

Make one user journey work completely, from an empty screen through validation and recovery.

By Randy Salars Β· Published

On this page
  1. Build the whole small journey
  2. Start with a complete file
  3. Trace a successful addition
  4. Treat a title as text
  5. Run a real user-journey check
  6. Add persistence only when the requirement changes
  7. Keep AI assistance tied to a specific change
  8. A reusable prompt
  9. For students: demonstrate the application without narrating excuses
  10. Practice: complete and inspect one journey

Make one user journey work completely, from an empty screen through validation and recovery.

A working application is a conversation between a person and a system. The person expresses an intention, the system responds, and the person needs to understand what happened. A polished opening screen is only the beginning of that conversation.

Our project is the small catalog specified in Article 104. A reader enters a title, adds it to a list, and can remove it. The list accepts at most 20 titles, rejects duplicates without regard to case, and limits each trimmed title to 60 Unicode code points. Refreshing the page clears the list.

That last behavior is part of the product definition. This is a local prototype with data in memory. It does not claim to save records for tomorrow or share them with another person.

Build the whole small journey

Before choosing colors, write down the states a user can encounter. The empty state should explain how to begin. A successful addition should appear in the list and produce a clear confirmation. Invalid input should remain available to edit, with a specific explanation. Removing an item should update both the list and the count.

The input should have a visible label. Keyboard users should be able to submit the form and activate removal buttons. After a change, focus should land somewhere useful. These details are part of the core behavior, not decoration added after the application works.

W3C's guidance on form notifications explains ways to identify errors and communicate status changes. The example below uses a visible error region and a separate status region. That implementation still needs testing with the browsers and assistive technologies used by its intended audience.

Start with a complete file

Save this as catalog.html and open it in a current browser. It has no external libraries, network requests, build step, or credentials. The code is included in full so you can trace the behavior from input to displayed result.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Practice catalog</title>
  <style>
    * { box-sizing: border-box; }
    body { font: 1rem/1.5 system-ui, sans-serif; max-width: 44rem;
           margin: auto; padding: 1rem; color: #17212b; background: #fff; }
    input, button { font: inherit; padding: .6rem; min-height: 2.75rem; }
    input { width: 100%; border: 2px solid #52606d; }
    button { cursor: pointer; margin-top: .5rem; }
    :focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
    #error { color: #a01616; font-weight: 600; }
    ul { padding: 0; list-style: none; }
    li { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;
         border-bottom: 1px solid #52606d; padding: .75rem 0; }
    li span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
    li button { flex-shrink: 0; }
  </style>
</head>
<body>
  <main>
    <h1>Practice catalog</h1>
    <p>Use sample titles. Entries stay in this tab's memory and disappear on reload.</p>
    <form id="entry-form" novalidate>
      <label for="title">Item title</label>
      <p id="hint">Enter 1–60 characters. Maximum 20 entries.</p>
      <input id="title" name="title" required aria-describedby="hint error">
      <p id="error" role="alert"></p>
      <button type="submit">Add item</button>
    </form>
    <p id="status" role="status" aria-live="polite"></p>
    <h2>Items <span id="count">(0)</span></h2>
    <p id="empty">No items yet.</p>
    <ul id="items" aria-label="Catalog items"></ul>
  </main>
  <script>
    "use strict";
    const form = document.querySelector("#entry-form");
    const input = document.querySelector("#title");
    const error = document.querySelector("#error");
    const status = document.querySelector("#status");
    const list = document.querySelector("#items");
    let entries = [];
    let nextId = 1;
    function render() {
      list.replaceChildren();
      document.querySelector("#count").textContent = `(${entries.length})`;
      document.querySelector("#empty").hidden = entries.length !== 0;
      for (const entry of entries) {
        const row = document.createElement("li");
        const title = document.createElement("span");
        title.textContent = entry.title;
        const remove = document.createElement("button");
        remove.type = "button";
        remove.textContent = "Remove";
        remove.setAttribute("aria-label", `Remove ${entry.title}`);
        remove.addEventListener("click", () => {
          entries = entries.filter(item => item.id !== entry.id);
          render();
          error.textContent = "";
          input.removeAttribute("aria-invalid");
          status.textContent = `Removed ${entry.title}. ${entries.length} items remain.`;
          input.focus();
        });
        row.append(title, remove);
        list.append(row);
      }
    }
    form.addEventListener("submit", event => {
      event.preventDefault();
      const title = input.value.trim();
      let problem = "";
      if (!title) problem = "Enter a title.";
      else if (Array.from(title).length > 60) problem = "Use 60 characters or fewer.";
      else if (entries.some(item => item.title.toLowerCase() === title.toLowerCase()))
        problem = "That title is already in the catalog.";
      else if (entries.length >= 20) problem = "The practice catalog is full (20 items).";
      if (problem) {
        error.textContent = problem;
        status.textContent = "";
        input.setAttribute("aria-invalid", "true");
        input.focus();
        return;
      }
      entries.push({id: nextId++, title});
      render();
      error.textContent = "";
      input.removeAttribute("aria-invalid");
      status.textContent = `Added ${title}. ${entries.length} items total.`;
      input.value = "";
      input.focus();
    });
    render();
  </script>
</body>
</html>

Trace a successful addition

The submit handler prevents the form's default navigation. It reads the input, trims surrounding whitespace, and checks the rules in a defined order. Only a valid title reaches the line that adds an entry.

Each entry receives a stable numeric identifier. Removal uses that identifier rather than the title or the row's current position. This keeps the action connected to the intended entry when other entries are removed.

The render function rebuilds the visible list from the current entries array. For a maximum of 20 simple rows, that approach is easy to understand. A more complex interface may need to preserve focus within a row or update only changed elements. Here, the specified focus destination after removal is the title input, and the handler sets it explicitly.

The data flow is short enough to trace by hand: input text becomes a validated title; the title becomes a record in the array; the array becomes visible rows. If an item appears twice, investigate the validation and state changes. If the array contains the right record but the screen does not, investigate rendering.

Treat a title as text

The example assigns the title with textContent. It does not concatenate the title into an HTML string. Consequently, an entry such as <strong>Map</strong> is intended to appear literally, including its angle brackets.

This distinction matters whenever a person or model supplies display content. A string may resemble markup without being authorized to become markup. MDN's textContent documentation explains the difference from properties that parse HTML.

The length rule counts Unicode code points using Array.from(title).length. That count can differ from the number of visible symbols a person perceives, because some symbols use multiple code points. For an international application, decide whether the product needs code points, grapheme clusters, or another definition, then align the instructions, implementation, and tests.

Likewise, lowercasing is a simple duplicate rule, not a full language-aware identity system. Accents, normalization forms, and locale-specific case rules require further specification. The current prototype makes a small, reproducible choice that students can inspect.

Run a real user-journey check

The HTML received code review during preparation, but browser execution was blocked by the available browser's local-page policy. The following checks are therefore instructions for the reader, not a report of completed browser testing.

Open the saved file and work through this sequence:

  1. Submit an empty title. Confirm that the error asks for a title and no item appears.
  2. Enter Field notebook and submit with Enter. Confirm the displayed title has no surrounding spaces, the count is one, and the input is cleared.
  3. Enter field notebook. Confirm the duplicate error and unchanged count.
  4. Enter 61 ordinary letters. Confirm the length error and that the text remains available to edit.
  5. Enter <strong>Map</strong>. Confirm that it appears as literal text, with no HTML formatting applied.
  6. Reach a removal button using the keyboard and activate it with Enter. Confirm the intended item disappears and focus returns to the input.
  7. Refresh the page. Confirm that the empty state returns.
  8. Add 20 distinct sample titles. Confirm that a twenty-first is rejected and that removing one permits another addition.

Then inspect the page at a narrow mobile width and a desktop width. Use a long unbroken title. Look for horizontal overflow, clipped text, overlapping controls, and a visible focus indicator. Increase browser zoom and repeat the central journey.

A screen reader check should verify that the input label is announced, each removal button identifies its item, and errors and status changes are understandable. Passing a keyboard test alone does not establish full accessibility.

Add persistence only when the requirement changes

A reader may now ask, β€œCan it remember my list?” That request changes the data model and failure states.

Browser storage might support a personal local list, but the application would need to handle unavailable storage, malformed stored content, and expectations about clearing browser data. A server-backed list introduces authentication, access control, network failures, and conflicts between edits.

Write the new acceptance criteria first. What happens if saving fails? Does the interface show an unsaved item? Can two people edit the same record? Who may read it? What should happen after signing out?

The current prototype has no asynchronous saving step, so a loading spinner would imply work that does not exist. When you add a real network operation, add the corresponding pending, success, failure, and retry behavior. A visual state should communicate actual system state.

Keep AI assistance tied to a specific change

A useful request might be: β€œAdd a character counter that uses the same counting rule as validation, and explain where it updates.” That change has a clear purpose and a small review surface.

A request to β€œmake this enterprise-ready” does not. It gives an assistant room to add accounts, frameworks, databases, analytics, and deployment settings without a concrete user need. Break the larger goal into capabilities and decide which one matters next.

After each change, repeat the affected acceptance checks. Adding a counter should not erase duplicate detection. Changing row layout should not make removal buttons unreachable by keyboard. A small application remains manageable when each iteration preserves a known working journey.

A reusable prompt

Build a small prototype for this specification: [specification]. Implement one complete user journey with empty, valid, invalid, and removal behavior. Explain where state lives and when it disappears. Use explicit labels, keyboard-operable controls, meaningful focus handling, and literal text rendering for user input. Provide complete code and a manual acceptance procedure. Separate code review from browser checks actually performed.

For students: demonstrate the application without narrating excuses

Ask a classmate to add a title, correct a mistake, and remove an item without explaining the interface first. Watch where they hesitate. Their confusion is evidence about the design, even when the code behaves as written.

For an assessed project, map each requirement to an observable check. Identify the AI-generated parts and the changes you made after testing. If browser testing was unavailable, state that limitation and provide the procedure another person can run.

Students from non-computing fields can adapt the same journey to a reading list, specimen-label practice, exhibition ideas, or vocabulary collection. Use sample content until storage and access requirements have been addressed.

Practice: complete and inspect one journey

Save and open the file, then perform the eight checks above. Record expected and observed behavior separately. Fix one confirmed defect or make one small, specified improvement, and repeat the affected checks.

Completion check: Another person can add, correct, and remove sample entries; you can explain the list's lifetime and show evidence for the checks you claim passed.

Stretch: Specify persistent storage without implementing it yet. Include a failed save, a reload, and a conflicting edit. Explain how each state would appear to the user.

Get the AI Dispatch

Weekly insights on ai & technology β€” delivered to your inbox. No spam, unsubscribe any time.

Want to choose specific topics? Customize your interests