Build a Browser Project Explorer

Build a responsive web app that loads local JSON and handles HTTP errors.

Introduction

30 Second Summary

A webpage can look finished while its most important button quietly leads nowhere. When data fails to arrive, the useful clues sit beneath the visible page.

In this project, you will build a responsive Project Explorer using browser-native technologies. You will trace a local JSON request from a button click through HTTP status handling to a visible DOM update.

What You'll Build

Selecting Load projects fills the page with three accessible cards while the status reads 3 projects loaded. plus DevTools exposes the successful request.

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

  • A keyboard-usable interface that lets you reach Load projects with the Tab key while every section stays readable at desktop and narrow browser widths.
  • A working local data flow that shows clear loading feedback before three records appear as project cards.
  • A repeatable debugging workflow that lets you trace a deliberate 404, inspect the failed response, then confirm the repaired request with 200.
  • Secret Mission: A live text filter that narrows the project cards as you type while reporting matching and empty results.

Are there any prerequisites?

With Visual Studio Code plus Python already available on Windows, either Microsoft Edge or Google Chrome completes the toolkit without an installation.

Before We Start

Before any hands-on work begins, this step is your chance to commit to the Project Explorer's purpose. You'll define the path from a user action through an HTTP response to a DOM update before adding an AI API.

Start a Local Web Workspace

The Project Explorer needs a local HTTP origin before the browser can request its files. Opening loose files directly would skip the server side of the flow you want to understand.

You'll create the workspace in Visual Studio Code. Python's local server gives the browser a stable address for every file.

In this step, get ready to:
  • Create the browser-fundamentals-lab workspace in Visual Studio Code.
  • Add the starter HTML page with empty stylesheet and script files.
  • Serve the workspace locally and load it through the browser.
Create the project folder and starter page

A Visual Studio Code workspace keeps the page and its supporting files in one known location. Starting on your Desktop also makes the project easy to find outside the editor.

  • Press the Windows key to open Windows Search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Click File in the top menu bar.
  • Select Open Folder.
  • Select Desktop in the folder picker.

Keep the workspace together

Every file for this project belongs inside one folder. Visual Studio Code may also show a workspace trust prompt after that folder opens.

  • Click the New folder icon in the folder picker.
  • Type browser-fundamentals-lab as the folder name.
  • Press Enter to create the folder.
  • Click Select Folder to open the new folder.
  • Confirm that you trust the folder if Visual Studio Code displays a workspace trust prompt.
  • Click the New File icon in the Explorer sidebar.
  • Type index.html as the file name.
  • Press Enter to create the file.

Start with a small document

The first page only needs its document metadata and visible heading. This creates a small result that you can confirm before the workspace gains more files.

  • Build the starter page by pasting this code into index.html:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Project Explorer</title>
  </head>
  <body>
    <h1>Project Explorer</h1>
  </body>
</html>

What does this markup establish?

  • The document metadata identifies the page as HTML and helps it fit the browser viewport.
  • The title element gives the browser tab its Project Explorer label.
  • The h1 element gives the page its visible Project Explorer heading.
  • Save index.html by pressing Ctrl+S.
  • Confirm that index.html appears beneath browser-fundamentals-lab in the Explorer sidebar.

Can't find the starter file?

  • Check that the Explorer sidebar shows browser-fundamentals-lab as the open folder.
  • Check that the file is named index.html without an extra file extension.

Help me check why my Visual Studio Code workspace does not show index.html inside browser-fundamentals-lab.

Connect the stylesheet and script

The browser discovers supporting resources through links in the HTML document. You'll create dedicated files for CSS and JavaScript before connecting them to the page.

  • Click the New File icon in the Explorer sidebar.
  • Type styles.css as the file name.
  • Press Enter to create the empty stylesheet.
  • Click the New File icon again.
  • Type app.js as the file name.
  • Press Enter to create the empty script.

The Explorer sidebar now lists three project files. The empty stylesheet and script give the browser stable resource paths for later changes.

  • Click index.html in the Explorer sidebar.
  • Press Ctrl+A inside the editor to select the current markup.
  • Connect all three project files by replacing the selected markup with this code:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Project Explorer</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <h1>Project Explorer</h1>

    <script src="app.js"></script>
  </body>
</html>

How are the files connected?

  • The link element tells the browser to request styles.css for the page's presentation.
  • The script element tells the browser to request app.js after reading the visible heading.
  • The relative file names point to resources in the same browser-fundamentals-lab folder.
  • Save index.html by pressing Ctrl+S.
  • Confirm that the Explorer sidebar lists index.html, styles.css, and app.js.

The styles.css and app.js files remain empty. Your workspace now matches the starter structure needed by the local server.

Files not connecting correctly?

  • Check that all three files appear at the same level in the Explorer sidebar.
  • Check that each filename uses the exact spelling shown in index.html.
  • Remove any accidental .txt extension from the stylesheet or script file.

Help me compare my three starter files and find why index.html cannot connect to styles.css or app.js.

✔️ Awesome, I've got everything!

Your starter document is saved. The empty stylesheet and script are ready beside it.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Project Explorer</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <h1>Project Explorer</h1>

    <script src="app.js"></script>
  </body>
</html>

What should the files contain?

Your index.html file should match this reference exactly. Your styles.css and app.js files should both be empty.

Serve the workspace over HTTP

Python's standard-library server maps browser requests to files in the open project folder. Binding it to 127.0.0.1 keeps the development server on your own computer.

  • Click Terminal in the Visual Studio Code menu bar.
  • Select New Terminal.
  • Confirm that the terminal prompt points to the browser-fundamentals-lab folder.
  • Start the local development server by running this command:
python -m http.server --bind 127.0.0.1

What does this command do?

  • Python starts its http.server module and serves files from the current folder.
  • The --bind option limits the server to the local 127.0.0.1 address.
  • The server listens on port 8000 because no different port was supplied.
  • The process keeps the integrated terminal occupied while the server is running.

The terminal now shows that the server is listening on port 8000.

  • Leave the server process running in the integrated terminal.

Server command not starting?

  • Check that the terminal prompt points to browser-fundamentals-lab before rerunning the command.
  • Close any other development server already using port 8000.
  • Confirm that you entered the command with python in lowercase.

Help me troubleshoot why Python's local server is not starting from my browser-fundamentals-lab folder.

Before you visit the local address, what do you expect the browser and terminal to show when the request succeeds?

  • Press the Windows key to open Windows Search.
  • Type Microsoft Edge or Google Chrome into the search field.
  • Press Enter to open the browser.
  • Click the browser's address bar.
  • Type http://127.0.0.1:8000/index.html into the address bar.
  • Press Enter to request the starter page.

You'll see the Project Explorer heading at the local address. The integrated terminal logs the browser request for /index.html.

Your local development loop is working. The browser can now request project files through Python instead of reading them directly from disk.

Starter page not loading?

  • Check that the integrated terminal still shows the running server process.
  • Check that the browser address uses port 8000.
  • Check that index.html remains inside browser-fundamentals-lab.

Help me trace why http://127.0.0.1:8000/index.html does not show my Project Explorer heading.

Your browser now reaches a real local server and receives the starter page. Next, you'll turn that page into a semantic Project Explorer interface.

Build the Semantic Project Explorer

Your local HTTP loop is working. The browser now requests index.html from Python's development server.

This step turns the starter heading into a complete static interface with Semantic HTML and responsive CSS. The controls expose the next limitation because app.js has no behavior to retrieve changing data.

In this step, get ready to:
  • Replace the starter markup with semantic page regions.
  • Style the Project Explorer for desktop and narrow browser widths.
  • Trace the interface through DevTools before testing the inactive button.
Build the semantic page structure

Semantic elements describe the purpose of each page region. This structure gives the browser a clear map of the header, main content, controls, project list, and footer.

  • In index.html, replace the existing <h1> element with the hero markup below:
    <header class="hero">
      <div class="hero__content">
        <p class="eyebrow">Browser fundamentals lab</p>
        <h1>Project Explorer</h1>
        <p>Trace a request from a button click to JSON, status handling, and the DOM.</p>
      </div>
    </header>

What does this markup do?

  • The header element identifies the introductory region of the page.
  • The hero__content wrapper controls the width of the heading content.
  • The eyebrow text gives the interface a short lab label.
  • Save index.html.
  • Return to the browser from the previous step.
  • Refresh the page.

You should see the lab label above the Project Explorer heading. A sentence about tracing a request should appear below it.

Still seeing only the heading?

Confirm that the new header sits inside body. Save index.html before refreshing the browser.

Check that you refreshed the page served at http://127.0.0.1:8000/index.html.

Help me compare my hero markup with the expected HTML.

The hero introduces the page. The main region now needs controls plus a destination for the project records.

  • In index.html, add this markup below the closing </header> tag:
    <main>
      <section class="panel controls" aria-labelledby="controls-heading">
        <div>
          <h2 id="controls-heading">Project data</h2>
          <p id="status-message" class="status" aria-live="polite">
            Ready to load projects.
          </p>
        </div>
        <button id="load-button" type="button">Load projects</button>
      </section>

      <section class="panel" aria-labelledby="projects-heading">
        <h2 id="projects-heading">Projects</h2>
        <ul id="project-list" class="project-grid"></ul>
      </section>
    </main>

How is the interface organized?

  • The main element contains the primary page content.
  • The native button uses type="button" so it has no form submission behavior.
  • The aria-live="polite" region lets assistive technology announce future status updates at a suitable pause.
  • The empty project-list provides a destination for project cards.
  • Save index.html.
  • Refresh the browser.

You should see a Project data section with the ready status. You should also see a Load projects button above an empty Projects section.

Missing a page section?

Check that both section elements remain inside main. A misplaced closing tag can push content outside the intended region.

Help me find the mismatched tag in my Project Explorer markup.

A footer closes the page with a compact summary of the browser technologies used in the project.

  • In index.html, add this footer between the closing </main> tag and the existing script element:
    <footer>
      <p>Built with semantic HTML, CSS, JavaScript, JSON, and HTTP.</p>
    </footer>

What does the footer add?

The footer element identifies the closing information for the page. Its text summarizes the technologies behind the finished data flow.

  • Save index.html.
  • Refresh the browser.

You should see the technology summary below the Projects section. The complete page structure is now visible.

Footer appearing above Projects?

Place the footer after the closing </main> tag. Keep the script element after the footer.

Help me put the footer in the correct place in index.html.

✔️ Awesome, I've got everything!

That is the page skeleton complete. Your Project Explorer now has meaningful regions for every part of the interface.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Project Explorer</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <header class="hero">
      <div class="hero__content">
        <p class="eyebrow">Browser fundamentals lab</p>
        <h1>Project Explorer</h1>
        <p>Trace a request from a button click to JSON, status handling, and the DOM.</p>
      </div>
    </header>

    <main>
      <section class="panel controls" aria-labelledby="controls-heading">
        <div>
          <h2 id="controls-heading">Project data</h2>
          <p id="status-message" class="status" aria-live="polite">
            Ready to load projects.
          </p>
        </div>
        <button id="load-button" type="button">Load projects</button>
      </section>

      <section class="panel" aria-labelledby="projects-heading">
        <h2 id="projects-heading">Projects</h2>
        <ul id="project-list" class="project-grid"></ul>
      </section>
    </main>

    <footer>
      <p>Built with semantic HTML, CSS, JavaScript, JSON, and HTTP.</p>
    </footer>

    <script src="app.js"></script>
  </body>
</html>
Style the interface for every screen size

Responsive styles let the same page adapt to different browser widths. The layout also needs a visible focus indicator so keyboard users can locate the active control.

  • In the empty styles.css file, add the page foundation by copying this code:
:root {
  color-scheme: light;
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #172033;
  background: #eef2ff;
}

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  background: #eef2ff;
}

.hero {
  padding: 3.5rem 1rem;
  color: #ffffff;
  background: linear-gradient(135deg, #312e81, #2563eb);
}

What do these foundation styles control?

  • The root rule establishes the page colors plus its font stack.
  • The universal selector makes element dimensions include their borders plus padding.
  • The body fills the viewport with the pale page background.
  • The hero uses a blue gradient with white text.
  • Save styles.css.
  • Refresh the browser.

You should see a pale page background. The hero should have a blue gradient with white text.

Still seeing an unstyled page?

Confirm that styles.css is beside index.html. Check that the stylesheet link still uses href="styles.css".

Help me find why styles.css is not loading.

The next styles constrain the content width. They also create a clear typography hierarchy inside the hero.

  • In styles.css, add this code below the existing hero rule:
.hero__content,
main,
footer {
  width: min(70rem, 92vw);
  margin-inline: auto;
}

.eyebrow {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0.75rem;
  font-size: clamp(2.2rem, 6vw, 4rem);
}

How does the page stay readable?

  • The shared width rule keeps the page content centered.
  • The eyebrow rule turns the lab label into a compact visual marker.
  • The heading size scales with the viewport while staying inside fixed limits.
  • Save styles.css.
  • Refresh the browser.

You should see centered content with space at both sides. The main heading should scale without overflowing the hero.

Content touching the browser edge?

Check the shared selector for .hero__content, main, and footer. Confirm that its width uses 92vw.

Help me debug the centered content width.

Panels separate the controls from the future project cards. Flexible controls keep the status and button aligned without fixing either one to a specific width.

  • In styles.css, add the layout rules below the heading styles:
main {
  display: grid;
  gap: 1.25rem;
  padding-block: 2rem;
}

.panel {
  padding: 1.5rem;
  border: 1px solid #c7d2fe;
  border-radius: 1rem;
  background: #ffffff;
  box-shadow: 0 0.75rem 2rem rgb(49 46 129 / 0.08);
}

.controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.status {
  margin-bottom: 0;
  color: #475569;
}

How do the panels work?

  • The main grid creates consistent space between the two sections.
  • Each panel gets a white surface with a border plus a soft shadow.
  • The controls use a flexible row to separate the status from the button.
  • The status uses a muted color to preserve the heading hierarchy.
  • Save styles.css.
  • Refresh the browser.

You should see two white panels below the hero. The status and Load projects button should sit on opposite sides of the first panel.

Panels missing their spacing?

Check that both sections still use the panel class. Confirm that the Project data section also uses the controls class.

Help me debug the panel layout classes.

The button needs clear hover, focus, and disabled states. The focus outline provides a visible signal when someone navigates with a keyboard.

  • In styles.css, add these button rules below the status rule:
button {
  min-height: 2.75rem;
  padding: 0.7rem 1rem;
  border: 0;
  border-radius: 0.65rem;
  color: #ffffff;
  background: #3730a3;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

button:hover {
  background: #312e81;
}

button:focus-visible,
input:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

button:disabled {
  cursor: wait;
  opacity: 0.65;
}

What do the control states communicate?

  • The base rule gives the button a large click target plus readable contrast.
  • The hover rule darkens the background under a pointer.
  • The focus rule adds an amber outline during keyboard navigation.
  • The disabled rule prepares the button for a future loading state.
  • Save styles.css.
  • Refresh the browser.
  • Press Tab until the Load projects button receives focus.

You should see an amber outline around the button. The outline should sit slightly outside its edge.

No focus outline on the button?

Click an empty part of the page before pressing Tab again. Check that the selector is written as button:focus-visible.

Help me debug the keyboard focus state.

The empty project region needs a layout ready for any number of cards. An auto-fitting grid can add columns whenever enough horizontal space is available.

  • In styles.css, add the project grid rules below the button styles:
.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
  padding: 0;
  margin: 0;
  list-style: none;
}

.project-card {
  height: 100%;
  padding: 1.25rem;
  border: 1px solid #dbeafe;
  border-left: 0.35rem solid #2563eb;
  border-radius: 0.75rem;
  background: #f8fafc;
}

.project-card h3 {
  color: #312e81;
}

.project-focus {
  margin-bottom: 0;
  font-weight: 700;
}

How will the card grid adapt?

  • The project list creates as many card columns as the available width supports.
  • Each column stays at least 15rem wide.
  • Each future card gets a pale surface with a blue accent border.
  • The focus text uses a heavier weight inside each card.
  • Save styles.css.
  • Refresh the browser.
  • Right-click the empty Projects panel.
  • Select Inspect.
  • Select the ul#project-list element in the HTML tree.

In the styles area, you should see the .project-grid rule applying a grid layout to the empty list.

Project grid rule missing?

Confirm that the list uses class="project-grid". Check that the selector in styles.css begins with a full stop.

Help me connect the project list to its CSS rule.

The final rules add footer spacing. A media query stacks the controls when the viewport becomes narrow.

  • In styles.css, add the responsive rules below the project card styles:
footer {
  padding-block: 0 2rem;
  color: #475569;
}

@media (max-width: 40rem) {
  .controls {
    align-items: stretch;
    flex-direction: column;
  }

  button {
    width: 100%;
  }
}

What changes on narrow screens?

  • The footer gets space below its summary text.
  • The controls switch from a horizontal row to a vertical column below 40rem.
  • The button expands across the narrow panel.
  • Save styles.css.
  • Refresh the browser.
  • Drag the browser window's right edge inward until the controls stack.

You should see the Load projects button move below the status. The button should span the width of the controls panel.

Controls staying in one row?

Narrow the browser window further. Confirm that the media query uses max-width: 40rem.

Help me debug the narrow-screen layout.

✔️ Awesome, I've got everything!

Your responsive styling is complete. The interface now adapts between wide and narrow browser windows.

ⓧ I'd like to double check the full code

:root {
  color-scheme: light;
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #172033;
  background: #eef2ff;
}

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  background: #eef2ff;
}

.hero {
  padding: 3.5rem 1rem;
  color: #ffffff;
  background: linear-gradient(135deg, #312e81, #2563eb);
}

.hero__content,
main,
footer {
  width: min(70rem, 92vw);
  margin-inline: auto;
}

.eyebrow {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0.75rem;
  font-size: clamp(2.2rem, 6vw, 4rem);
}

main {
  display: grid;
  gap: 1.25rem;
  padding-block: 2rem;
}

.panel {
  padding: 1.5rem;
  border: 1px solid #c7d2fe;
  border-radius: 1rem;
  background: #ffffff;
  box-shadow: 0 0.75rem 2rem rgb(49 46 129 / 0.08);
}

.controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.status {
  margin-bottom: 0;
  color: #475569;
}

button {
  min-height: 2.75rem;
  padding: 0.7rem 1rem;
  border: 0;
  border-radius: 0.65rem;
  color: #ffffff;
  background: #3730a3;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

button:hover {
  background: #312e81;
}

button:focus-visible,
input:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

button:disabled {
  cursor: wait;
  opacity: 0.65;
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
  padding: 0;
  margin: 0;
  list-style: none;
}

.project-card {
  height: 100%;
  padding: 1.25rem;
  border: 1px solid #dbeafe;
  border-left: 0.35rem solid #2563eb;
  border-radius: 0.75rem;
  background: #f8fafc;
}

.project-card h3 {
  color: #312e81;
}

.project-focus {
  margin-bottom: 0;
  font-weight: 700;
}

footer {
  padding-block: 0 2rem;
  color: #475569;
}

@media (max-width: 40rem) {
  .controls {
    align-items: stretch;
    flex-direction: column;
  }

  button {
    width: 100%;
  }
}
Inspect the browser's HTML tree

Browser DevTools connects the rendered interface to its underlying DOM tree. This view helps you confirm that the semantic elements reached the browser in the intended order.

  • Restore the browser to a comfortable desktop width.
  • Refresh the page.
  • Press Tab until Load projects receives focus.

The heading, ready status, focused button, and empty Projects panel should remain readable. The button should show its amber focus outline.

  • Right-click the page.
  • Select Inspect.
  • Select the Elements panel.
  • Expand the body element.
  • Locate the header element.
  • Locate the main element.
  • Locate the footer element.

You should see the three semantic regions in the same order as index.html. Inside main, the tree should include #status-message, #load-button, and the empty #project-list.

HTML tree different from the editor?

Save index.html before refreshing. Confirm that DevTools is attached to the page at http://127.0.0.1:8000/index.html.

Help me compare the browser DOM with index.html.

Before you click Load projects, do you think the status or project list will change?

  • Click Load projects.

The status stays at Ready to load projects. The Projects panel remains empty because app.js contains no behavior yet.

Your static Project Explorer is complete. Next, you'll connect the button to a request and trace a broken data path through the browser.

Follow a Broken Request

Your responsive Project Explorer now has semantic structure with clear controls. The Load projects button still has no behavior.

A polished page can hide a broken HTTP data path. DevTools can expose what the server returns for a JSON request.

In this step, get ready to:
  • Create a local JSON resource with three project records.
  • Connect the load button to an asynchronous request.
  • Trace a failed request through the Network tool.
Create the JSON resource

A JSON resource keeps the project records separate from the page structure. Each project becomes one object with a title, summary, and focus.

  • In the file sidebar of Visual Studio Code, use the folder creation icon to create a folder named data inside browser-fundamentals-lab.
  • Inside data, use the file creation icon to create projects.json.

The new data/projects.json path is now visible in the file sidebar.

  • Fill data/projects.json with the three project records by replacing its contents with this code:
[
  {
    "title": "Accessible Profile Form",
    "summary": "A keyboard-friendly form with native validation and clear feedback.",
    "focus": "Semantic HTML and accessibility"
  },
  {
    "title": "Request Inspector",
    "summary": "A small interface that traces a JSON request through loading, success, and failure.",
    "focus": "HTTP and debugging"
  },
  {
    "title": "Async Reading List",
    "summary": "A data-driven list that separates retrieval, transformation, and rendering.",
    "focus": "Promises and DOM updates"
  }
]

What does this data contain?

  • The outer array holds all three project records.
  • Each object contains the same title, summary, and focus fields.
  • The consistent fields give the browser a predictable shape to parse.
  • Save data/projects.json.
  • Return to the browser tab from earlier.
  • Enter http://127.0.0.1:8000/data/projects.json in the address bar.
  • Press Enter.

Good work. Your three records are now available through the same local HTTP origin as the page.

Cannot see the project records?

  • Confirm that projects.json sits inside the data folder.
  • Return to the server terminal from earlier to confirm that it remains active.
  • Check that the browser address contains the plural filename projects.json.

Help me find why my local JSON resource does not load in the browser.

✔️ Awesome, I've got everything!

Great. Double-check that you saved data/projects.json.

ⓧ I'd like to double check the full code

[
  {
    "title": "Accessible Profile Form",
    "summary": "A keyboard-friendly form with native validation and clear feedback.",
    "focus": "Semantic HTML and accessibility"
  },
  {
    "title": "Request Inspector",
    "summary": "A small interface that traces a JSON request through loading, success, and failure.",
    "focus": "HTTP and debugging"
  },
  {
    "title": "Async Reading List",
    "summary": "A data-driven list that separates retrieval, transformation, and rendering.",
    "focus": "Promises and DOM updates"
  }
]
Wire the first request

The browser's fetch() API returns a Promise for a Response. The async and await keywords keep the request sequence readable.

  • Return to app.js in Visual Studio Code.
  • Replace the empty file with the request code below:
const dataUrl = "./data/project.json";
const loadButton = document.querySelector("#load-button");
const statusMessage = document.querySelector("#status-message");
const projectList = document.querySelector("#project-list");

function setStatus(message) {
  statusMessage.textContent = message;
}

async function loadProjects() {
  const response = await fetch(dataUrl);
  const projects = await response.json();
  setStatus(`${projects.length} projects loaded.`);
}

loadButton.addEventListener("click", loadProjects);

What does this code do?

  • The dataUrl constant holds the resource path requested by fetch().
  • The loadButton reference locates the existing button in the DOM.
  • The statusMessage reference locates the polite live region.
  • The projectList reference locates the empty project list.
  • The setStatus() function changes the live region through textContent.
  • The loadProjects() function pauses at await until the response arrives.
  • The response.json() call parses the response body as JSON.
  • The addEventListener call runs loadProjects when you click the button.
  • Save app.js.
  • Return to the Project Explorer browser tab.
  • Refresh the page.
  • Right-click the page.
  • Select Inspect.
  • Select the Network tab.

You'll see the Network request log ready to capture the button's request.

Before you click Load projects, do you expect the request to produce project records or expose a problem?

  • Click Load projects.

You'll see project.json recorded with status 404. No project cards appear.

This failure is intentional. It proves that the button now reaches the browser's request layer.

Why Does Fetch Continue After a 404?

A server response such as 404 fulfills the Promise returned by fetch(). The response status needs its own explicit check.

The current response.json() call reads the error response body as JSON. That body cannot provide the expected project array.

No Failed Request in Network?

  • Keep the Network tab open before clicking the button again.
  • Confirm that app.js is saved.
  • Refresh the Project Explorer page after saving the script.

Help me find why clicking Load projects does not create a Network request.

✔️ Awesome, I've got everything!

Great. Double-check that you saved app.js.

ⓧ I'd like to double check the full code

const dataUrl = "./data/project.json";
const loadButton = document.querySelector("#load-button");
const statusMessage = document.querySelector("#status-message");
const projectList = document.querySelector("#project-list");

function setStatus(message) {
  statusMessage.textContent = message;
}

async function loadProjects() {
  const response = await fetch(dataUrl);
  const projects = await response.json();
  setStatus(`${projects.length} projects loaded.`);
}

loadButton.addEventListener("click", loadProjects);
Trace the 404 in Network

The 404 status proves that the local server received the request. The request details reveal which resource the browser asked it to find.

  • Select the project.json request in the Network log.
  • Read the value in the Status column.
  • Select the Headers panel.
  • Find the singular filename in the requested resource details.
  • Select the Response panel.
  • Read the server's response body.

You'll find 404 in the Status column. The request details identify the resource as project.json.

The Response panel reports File not found. This confirms that the server cannot map the requested path to a file.

  • Return to Visual Studio Code's file sidebar.
  • Compare project.json in the request with projects.json on disk.

The request drops the final s from the real filename. That one-character mismatch sends the server to a path that does not exist.

What Does the Evidence Prove?

The local server maps request paths to files inside browser-fundamentals-lab. A missing path produces a 404 response.

The Network evidence connects the button click to the requested filename. You can now diagnose the empty page from the browser's request trail.

Before you trigger the request again, which filename do you expect Network to record?

  • Return to the Project Explorer page.
  • Click Load projects.

Network records another project.json request. The Status column shows 404 again.

No cards appear in #project-list. You have reproduced the broken data path from the user action through the HTTP response.

You have followed a browser request from the button to a specific 404 response. Next, you'll correct the path and turn every request outcome into clear interface feedback.

Make the Data Flow Resilient

Your deliberate 404 proved that a healthy-looking page can still have a broken data path. The Network tool exposed the missing resource while the interface stayed empty.

The Fetch API fulfills its promise even when the server returns a 404 response. Your code therefore needs to check response.ok before parsing JSON.

You'll add that status check to the request flow. You'll also turn the records into cards through the DOM.

In this step, get ready to:
  • Correct the request path so it reaches the existing project data.
  • Render the project records as accessible cards.
  • Report successful requests or failed requests through the interface.
Correct the request path

The server maps the requested path to a file inside browser-fundamentals-lab. The path must match the plural filename in data/projects.json.

  • Switch back to app.js in Visual Studio Code.
  • Find the line containing const dataUrl = "./data/project.json";.
  • Replace the singular path so the line matches this code:
const dataUrl = "./data/projects.json";

Why One Letter Matters

The corrected URL now matches data/projects.json on disk. The local server can therefore return the resource with a successful HTTP status.

  • Save app.js.

Before you test the corrected path, do you expect the records to appear as cards yet? Hold that prediction in mind.

  • Refresh the Project Explorer in your browser.
  • Click Load projects.

You'll see 200 in the Status column. That is your first recovery win: the browser can reach all three records again.

The status reads 3 projects loaded. The project list remains empty because the current function does not send those records to a renderer.

Still Seeing 404?

  • Check that the requested path ends with projects.json.
  • Confirm that projects.json remains inside the data folder.

Help me trace why my corrected local JSON request still returns 404.

Build the resilient render flow

Each project becomes a list item containing an article. Assigning record values through textContent keeps those values as text instead of parsing them as markup.

  • In app.js, locate the closing brace of setStatus(message).
  • Paste these rendering functions immediately below it:
function createProjectCard(project) {
  const item = document.createElement("li");
  const article = document.createElement("article");
  const title = document.createElement("h3");
  const summary = document.createElement("p");
  const focus = document.createElement("p");

  article.className = "project-card";
  title.textContent = project.title;
  summary.textContent = project.summary;
  focus.className = "project-focus";
  focus.textContent = `Focus: ${project.focus}`;

  article.appendChild(title);
  article.appendChild(summary);
  article.appendChild(focus);
  item.appendChild(article);

  return item;
}

function renderProjects(projects) {
  projectList.replaceChildren();

  for (const project of projects) {
    projectList.appendChild(createProjectCard(project));
  }
}

What Does This Code Do?

  • The createProjectCard() function creates the list item that represents one project.
  • The element properties apply the existing card classes plus the project text.
  • The appendChild() calls assemble the card in a semantic order.
  • The renderProjects() function clears old cards with replaceChildren() before adding the current records.
  • Save app.js.
  • Confirm the saved file contains createProjectCard() immediately above renderProjects().

Can't Find Both Functions?

  • Check that both functions sit after setStatus().
  • Check that each function has its own closing brace.

Help me check the placement and braces for my project card rendering functions.

The renderer now knows how to build cards. The request function must pass parsed records into it while managing loading or failure states.

  • In app.js, locate the existing async function loadProjects().
  • Replace that function plus the click listener below it with this code:
async function loadProjects() {
  loadButton.disabled = true;
  setStatus("Loading projects...");

  try {
    const response = await fetch(dataUrl);

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    const projects = await response.json();
    renderProjects(projects);
    setStatus(`${projects.length} projects loaded.`);
  } catch (error) {
    projectList.replaceChildren();
    setStatus(`Could not load projects. ${error.message}`);
  } finally {
    loadButton.disabled = false;
  }
}

loadButton.addEventListener("click", loadProjects);
loadProjects();

How Does the Request Stay Resilient?

  • The disabled button prevents another click while the current request is active.
  • The response.ok check turns an unsuccessful status into an error containing response.status.
  • The catch block clears stale cards before reporting the failure through the existing polite live region.
  • The finally block restores the button after either outcome.
  • The final loadProjects() call loads the records when the page starts.
  • Save app.js.

Before you refresh, do you expect the corrected request to update the status before the cards appear? Keep your prediction in mind.

  • Refresh the Project Explorer in your browser.
  • Click Load projects to repeat the request.

You'll see three project cards in the grid. The status reads 3 projects loaded..

The Network tool records 200 for projects.json. During the request, the button uses its disabled state while the status briefly reads Loading projects....

Cards Not Appearing?

  • Confirm that the Python server from earlier is still running at http://127.0.0.1:8000.
  • Check that renderProjects(projects) appears after the JSON parsing line.
  • Check that createProjectCard(project) returns item.

Help me debug why my successful projects.json request does not render project cards.

Prove both failure and recovery

A resilient request flow needs a visible failure state as well as a success state. This check briefly changes only the local request path, so data/projects.json remains untouched.

  • In app.js, replace ./data/projects.json with ./data/project.json.
  • Save app.js.

Before you refresh, do you expect the old cards to remain after the failed request? Hold that prediction in mind.

  • Refresh the Project Explorer in your browser.

You'll see Could not load projects. Request failed with status 404 in the status region. The project list is cleared.

The Network tool records 404 for project.json. This confirms that the interface now explains the same failure you traced in the browser.

The error path works. Restore the real path so the completed explorer finishes in its successful state.

  • In app.js, replace ./data/project.json with ./data/projects.json.
  • Save app.js.

Before the final refresh, do you expect the same render pipeline to recover without any other edits? Keep your prediction in mind.

  • Refresh the Project Explorer in your browser.
  • Click Load projects.

You'll see all three cards return. The status reads 3 projects loaded..

The Network tool records 200 for projects.json. That's the full browser loop working: user action, request, status check, JSON parsing, card rendering, and accessible feedback.

Still Seeing the Error State?

  • Confirm that dataUrl contains the plural filename projects.json.
  • Save app.js before refreshing the browser again.
  • Check that the latest projects.json row shows 200 in the Status column.

Help me restore my Project Explorer from the deliberate 404 to the successful three-card state.

✔️ Awesome, I've got everything!

Your saved app.js now contains the complete resilient request and rendering flow.

ⓧ I'd like to double check the full code

const dataUrl = "./data/projects.json";
const loadButton = document.querySelector("#load-button");
const statusMessage = document.querySelector("#status-message");
const projectList = document.querySelector("#project-list");

function setStatus(message) {
  statusMessage.textContent = message;
}

function createProjectCard(project) {
  const item = document.createElement("li");
  const article = document.createElement("article");
  const title = document.createElement("h3");
  const summary = document.createElement("p");
  const focus = document.createElement("p");

  article.className = "project-card";
  title.textContent = project.title;
  summary.textContent = project.summary;
  focus.className = "project-focus";
  focus.textContent = `Focus: ${project.focus}`;

  article.appendChild(title);
  article.appendChild(summary);
  article.appendChild(focus);
  item.appendChild(article);

  return item;
}

function renderProjects(projects) {
  projectList.replaceChildren();

  for (const project of projects) {
    projectList.appendChild(createProjectCard(project));
  }
}

async function loadProjects() {
  loadButton.disabled = true;
  setStatus("Loading projects...");

  try {
    const response = await fetch(dataUrl);

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    const projects = await response.json();
    renderProjects(projects);
    setStatus(`${projects.length} projects loaded.`);
  } catch (error) {
    projectList.replaceChildren();
    setStatus(`Could not load projects. ${error.message}`);
  } finally {
    loadButton.disabled = false;
  }
}

loadButton.addEventListener("click", loadProjects);
loadProjects();

Secret mission

Add Live Project Filtering

Turn the Project Explorer into a live search experience. Add a labeled filter that narrows the loaded cards with every keystroke while keeping the accessible status message in sync.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the app running, stop it for later, or remove it. This project uses no cloud resources, so there are no ongoing charges.

Resources you used:

  • The browser-fundamentals-lab folder containing your completed app files.
  • The temporary Python development server serving http://127.0.0.1:8000.

Keep everything running

No action needed. Choose this if you are still testing the Project Explorer or experimenting with its browser behavior.

  • Leave the Python server terminal running.
  • Keep browser-fundamentals-lab open in Visual Studio Code.
  • Continue testing the app at http://127.0.0.1:8000.

Keep the Server Local

Python's http.server supports basic local development. Python does not recommend it for production because it implements only basic security checks.

Pause - I'll come back to this later

Stop the local server to free its port. Your completed project files remain inside browser-fundamentals-lab.

  • Switch back to the Visual Studio Code integrated terminal from earlier.
  • Press Ctrl+C to stop the Python server.
  • Refresh http://127.0.0.1:8000 in your browser.

Your browser can no longer reach the local server. This confirms that the process has stopped.

  • Restart the server from browser-fundamentals-lab later by running this command:
python -m http.server --bind 127.0.0.1

What Does This Command Do?

The command serves the current project folder on port 8000. It limits the server to your computer through 127.0.0.1.

Delete - I don't want to use this again

Deleting the folder is permanent. It removes only this local lab plus its project files.

  • Switch back to the Visual Studio Code integrated terminal from earlier.
  • Press Ctrl+C to stop the Python server.
  • Select the dropdown beside the plus button in the integrated terminal panel.
  • Choose Command Prompt to start a terminal inside browser-fundamentals-lab.
  • Move out of the project folder and delete it by running these commands:
cd ..
rmdir /s /q browser-fundamentals-lab

What Do These Commands Remove?

The first line moves Command Prompt out of the project folder.

The second line permanently removes browser-fundamentals-lab plus every file inside it.

Command Prompt returns without an error when the deletion succeeds. Visual Studio Code can no longer read the deleted project files.

Folder Still Present?

  • Stop the Python server before retrying the deletion.
  • Confirm that the prompt no longer ends with browser-fundamentals-lab before retrying.
  • Ask for help diagnosing the deletion.

Nice Work!

Nice Work!

You did it! Your responsive Project Explorer now loads local JSON through HTTP.

What you learned:

  • Build a responsive interface with semantic HTML. Style flexible layouts with CSS. Preserve visible keyboard focus at narrow browser widths.
  • Connect a button event to an asynchronous request with JavaScript. Parse JSON data. Render accessible project cards through the DOM.
  • Trace an intentional 404 through the Network tool in DevTools. Use response.ok to turn request failures into clear status updates.
  • Complete the optional Secret Mission by adding live case-insensitive project filtering. Reuse renderProjects() while reporting zero, one, or multiple matches in the polite live region.

Ready to quiz yourself?