MGU Computer Science Series — Web Development: HTML, CSS, and JavaScript
Part V: Web Projects and Components
JSON · Fetch · Promises · Data-Driven Pages
Chapter 28 of 30
28
MGU Computer Science Series  ·  Web Development: HTML, CSS, and JavaScript  ·  Library Textbook Edition

Chapter 28:
Fetching Content and
Working with JSON

Separating content from code — JSON structure, local data files, the fetch() function, Promises, rendering data as HTML, and handling loading and error states

■ Part V of VII ■

■ 10 Sections  •  Chapter 28 of 30 ■

Builds on the DOM (Chapter 22), events (Chapter 21), and UI patterns (Chapter 27) — introductory level only

Chapter Introduction and Learning Objectives

Every website you have built so far has contained content that was written directly into HTML files. The text, the links, the images, the form labels — all of it lived inside the HTML you typed by hand. This approach works well for small sites. But many modern websites display content that changes often: news articles, product listings, user profiles, event schedules, blog posts, resource directories. Writing that content by hand into HTML every time something changes would be tedious, error-prone, and difficult to maintain.

There is a better approach. Websites can store content separately from the HTML that displays it, in a format called JSON (JavaScript Object Notation). JavaScript can then read that content and build the page dynamically. For content that lives on a remote server, JavaScript can fetch it over the network. This chapter introduces both JSON and fetching at a beginner level — vocabulary and habits that connect directly to later courses in JavaScript, front-end frameworks, APIs, databases, and full-stack development.

Opening Question

A resource directory page needs to display eight learning resources today. Next year it might need to display two hundred. A teammate wants to add a new resource without touching a single line of HTML. A visitor on a slow connection should never see a blank, unexplained gap on the page while content loads — and if the data fails to arrive at all, the page should still make sense. How do you build a page like that? The answer is to separate the content from the page that displays it, and to teach JavaScript how to load that content, render it, and respond gracefully when something goes wrong. That is exactly what this chapter teaches.

10Sections
10Objectives
13Key Terms
30Total Chapters

Learning Objectives

By the end of this chapter, you should be able to:

  1. 1Explain why modern websites separate data from presentation, and describe what data-driven thinking means for a web page.
  2. 2Define JSON and identify valid versus invalid JSON syntax.
  3. 3Distinguish between JSON objects, JSON arrays, and arrays of objects, including nested objects.
  4. 4Create and organize a local JSON data file within a web project's folder structure.
  5. 5Explain why fetch() requires a local development server rather than a bare file path.
  6. 6Describe how fetch() works and why network requests in JavaScript are asynchronous.
  7. 7Explain what a Promise is, its three states, and how .then() and .catch() handle results and errors.
  8. 8Write JavaScript that fetches a JSON file and renders its contents as HTML on the page.
  9. 9Implement loading and error states for a data-driven page component, including accessible feedback.
  10. 10Apply responsible practices for using external data and APIs, including attribution, sanitizing data, and respecting rate limits.

Sections at a Glance

Sections 28.1 – 28.3
The Case for Data: Why Separate Content from Code
Data-Driven Thinking · JSON Syntax · Objects and Arrays

Why Websites Use Data

Open any large website and look carefully at what you see. A news site has dozens of article summaries, each with a headline, a photo, a category, and a short description. An online store has hundreds of products, each with a name, a price, an image, and a stock status. A school website has a directory of staff, each with a name, a title, a department, and a photo.

If each of those items were written manually into HTML, maintaining the site would be a continuous struggle. Adding a new article would mean opening the HTML file, writing the markup, making sure the structure matches every other article, saving, uploading, and testing. Changing a price would mean hunting through the HTML for the right line. Removing a staff member who left would mean finding every page that mentioned them.

Instead, most websites separate content from presentation. Content lives in a structured data format. Presentation logic lives in HTML, CSS, and JavaScript templates. When the page loads, JavaScript reads the data and builds the HTML automatically. Changing a product price means changing a number in a data file, not editing HTML.

A Key Idea

This separation of data from presentation is one of the most important architectural ideas in web development. Even at a beginner level, learning to work with separate data makes your websites easier to update, extend, and eventually connect to real databases and APIs.

Data-Driven Thinking

When you think about a website as data-driven, you start asking a different set of questions:

  • What are the repeating units of content on this page?
  • What pieces of information does each unit need?
  • Where does that information come from?
  • When should it be loaded?
  • What should the page show while the data is loading?
  • What should the page show if the data fails to load?

These questions guide the work of this entire chapter.

What JSON Is

JSON stands for JavaScript Object Notation. It is a lightweight text format for storing and exchanging structured data. JSON looks similar to JavaScript objects and arrays, which makes it easy to read and write for anyone who has worked through the earlier chapters of this textbook.

JSON was designed to be readable by humans and easy to parse by machines. It is now the most widely used data format on the web. When one website or application sends data to another — through an API, a database query, or a file — JSON is usually the format used.

JSON files typically have a .json file extension. They are plain text files, which means you can open them in any text editor. They contain no HTML, no CSS, and no JavaScript — just structured data. Here is a simple example of JSON representing a single person:

JSON — a single object
{
  "name": "Amara Osei",
  "role": "Student",
  "year": 2,
  "active": true
}

This should look familiar. The structure uses curly braces, property names in double quotes, a colon between each name and value, and commas between properties. This is the same syntax as a JavaScript object. There are a few important rules for valid JSON:

  • All property names must be in double quotes. Single quotes are not allowed in JSON.
  • Values can be strings (in double quotes), numbers (without quotes), Booleans (true or false), null, arrays, or objects.
  • There must be no trailing commas. A comma after the last property in an object is a JSON error.
  • JSON does not support comments.
Invalid JSON — single quotes, unquoted keys, trailing comma
// INVALID JSON — do not do this
{
  name: "Amara Osei",
  role: 'Student',
  year: 2,
  active: true,
}
Corrected — valid JSON
{
  "name": "Amara Osei",
  "role": "Student",
  "year": 2,
  "active": true
}
Why These Rules Matter

Unlike JavaScript, which is flexible about many syntax issues, JSON is strict. A single missing double quote, a trailing comma, or an unquoted property name will cause a JSON parse error. This strictness is intentional: it keeps JSON simple and consistent across many different programming languages, not just JavaScript. When you write JSON by hand, always validate it. There are free online JSON validators you can paste your JSON into to check for errors.

Arrays and Objects in JSON

JSON supports two main structures: objects and arrays. These can be nested inside each other in any combination.

JSON Objects

A JSON object is a collection of key-value pairs. Keys are strings. Values can be any valid JSON value. Objects are wrapped in curly braces.

{
  "title": "Introduction to HTML",
  "chapter": 5,
  "published": true,
  "author": "MGU Editorial Team"
}

You can read this as: there is an object with four properties — title, chapter, published, and author.

JSON Arrays

A JSON array is an ordered list of values. Arrays are wrapped in square brackets. Each item is separated by a comma. Array items can be any valid JSON value — strings, numbers, Booleans, objects, or even other arrays.

[
  "HTML",
  "CSS",
  "JavaScript"
]

Arrays of Objects

The most common structure you will encounter is an array of objects. This represents a list of similar items, where each item has the same set of properties.

[
  {
    "id": 1,
    "title": "What Web Development Studies",
    "chapter": 1,
    "part": "Foundations of the Web"
  },
  {
    "id": 2,
    "title": "How the Web Works",
    "chapter": 2,
    "part": "Foundations of the Web"
  },
  {
    "id": 3,
    "title": "Files, Folders, Paths, and Project Structure",
    "chapter": 3,
    "part": "Foundations of the Web"
  }
]

This structure is extremely useful for pages like resource directories, product listings, team member rosters, event schedules, and course guides — anywhere you have a repeating collection of similar items.

Nested Objects

Objects can contain other objects as property values.

{
  "name": "Kofi Mensah",
  "contact": {
    "email": "[email protected]",
    "phone": "555-0102"
  },
  "department": "Computer Science"
}

Here, the contact property is itself an object with email and phone properties. To access email in JavaScript after parsing this JSON, you would write person.contact.email.

Reading JSON Structure

Before writing any code, practice reading JSON by answering these questions about a data structure: Is the top-level structure an object {} or an array []? If it is an array, what does each item look like? What are the property names? What type is each value — string, number, Boolean, array, or nested object? These reading habits will serve you well as JSON structures grow more complex.

Sections 28.4 – 28.6
The Mechanics: Files, fetch(), and Promises
Local Data · Network Requests · Asynchronous JavaScript

Local JSON Files

Before learning to fetch data from a remote server, you will work with local JSON files. A local JSON file lives inside your project folder, just like your CSS or JavaScript files.

Using a local JSON file is good for learning because it removes the complexity of network requests. You can focus on reading and rendering data without worrying about server errors, network delays, or CORS policies — concepts you will encounter when working with remote APIs in later courses.

Creating a JSON File

In your project folder, create a data folder. Inside it, create a file called resources.json.

web-project/ index.html css/ main.css js/ main.js data/ resources.json

Here is sample content for resources.json:

[
  {
    "id": 1,
    "title": "MDN Web Docs: HTML",
    "category": "HTML",
    "description": "Mozilla's comprehensive reference for HTML elements, attributes, and best practices.",
    "url": "https://developer.mozilla.org/en-US/docs/Web/HTML"
  },
  {
    "id": 2,
    "title": "MDN Web Docs: CSS",
    "category": "CSS",
    "description": "Mozilla's complete CSS reference including properties, selectors, and layout guides.",
    "url": "https://developer.mozilla.org/en-US/docs/Web/CSS"
  },
  {
    "id": 3,
    "title": "MDN Web Docs: JavaScript",
    "category": "JavaScript",
    "description": "Mozilla's JavaScript reference covering syntax, built-in objects, and browser APIs.",
    "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript"
  },
  {
    "id": 4,
    "title": "The A11Y Project",
    "category": "Accessibility",
    "description": "A community-driven effort to make digital accessibility easier to understand and practice.",
    "url": "https://www.a11yproject.com"
  },
  {
    "id": 5,
    "title": "CSS-Tricks: Flexbox Guide",
    "category": "CSS",
    "description": "A thorough visual guide to CSS Flexbox with diagrams and interactive examples.",
    "url": "https://css-tricks.com/snippets/css/a-guide-to-flexbox/"
  }
]

This file defines an array of five resource objects. Each has an id, a title, a category, a description, and a url.

What You Cannot Do with a Bare File Path

Later in this chapter, you will use fetch() to read this file. One important thing to understand early: fetch() uses HTTP to request files, which means your page must be served by a web server — even locally. You cannot simply open an HTML file by double-clicking it in your file manager and expect fetch() to load a local JSON file. Modern browsers block this for security reasons.

To work with fetch() locally, use a simple local development server. Most code editors, including Visual Studio Code with the Live Server extension, provide this. When your page is served at an address like http://localhost:5500 instead of accessed as a bare file path, fetch() will work correctly. This requirement is not a bug — it is consistent with how websites actually work. Your project runs locally on a small server, just as it would run on a real hosting server when published.

Introduction to fetch()

The fetch() function is the browser's built-in tool for making network requests from JavaScript. It can request HTML, JSON, images, or any other resource from a URL. At its most basic, fetch() takes a URL as its argument and returns a response.

fetch("data/resources.json");

However, this line alone does nothing visible. It sends the request but does not do anything with the response. To work with the response, you need to understand something about how fetch() works: it is asynchronous.

Why Asynchronous Matters

When a browser sends a network request, it does not freeze and wait for the response before continuing with other work. Waiting could take milliseconds or several seconds depending on server speed, file size, and network conditions. If the browser stopped everything while waiting, the page would become unresponsive.

Instead, the browser sends the request and continues running other JavaScript. When the response arrives, a callback function handles it. This is asynchronous behavior: the request and the response happen at different times, and JavaScript continues working in between. This is different from the synchronous code you have written before, where each line executes before the next one begins.

fetch() handles this through Promises, which are introduced in the next section.

A First Complete fetch() Example

Here is a complete, working example of using fetch() to load and log a JSON file. Read it carefully before studying the explanation below.

fetch("data/resources.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log(data);
  })
  .catch(function(error) {
    console.log("Error loading data:", error);
  });
Reading the Example Line by Line
  • Line 1fetch("data/resources.json") sends a request for the file at that path. It returns a Promise.
  • Lines 2–4.then(function(response) { return response.json(); }) — when the response arrives, parse its body as JSON. response.json() also returns a Promise.
  • Lines 5–7.then(function(data) { console.log(data); }) — when the JSON is fully parsed, run this function with the parsed data. At this point, data is a JavaScript array or object you can work with.
  • Lines 8–10.catch(function(error) { ... }) — if anything goes wrong at any step, run this function with the error.

If you open the browser console after loading the page, you should see the parsed array logged there.

Promises at an Introductory Level

A Promise is a JavaScript object that represents the eventual result of an asynchronous operation. It is called a Promise because it represents a commitment: the result is not here yet, but it will be here eventually — or the operation will fail and the Promise will tell you that too.

State 1
Pending
The operation has started but has not finished yet. The Promise is waiting.
State 2
Fulfilled
The operation completed successfully. The Promise delivers a result value.
State 3
Rejected
The operation failed. The Promise delivers an error.

You work with Promises using the .then() and .catch() methods. .then(callback) runs the callback function when the Promise is fulfilled; the callback receives the result value as its argument. .catch(callback) runs the callback function if the Promise is rejected at any point in the chain; the callback receives the error as its argument.

Chaining Promises

Promises can be chained. When a .then() callback returns another Promise, the next .then() in the chain waits for that inner Promise to resolve. This is exactly what happens with fetch():

fetch("data/resources.json")         // Returns a Promise for a Response
  .then(function(response) {
    return response.json();           // Returns a Promise for parsed JSON
  })
  .then(function(data) {
    // data is the fully parsed JSON value
    renderResources(data);
  })
  .catch(function(error) {
    showError(error);
  });

The chain reads in order: first fetch, then parse, then use the data. If anything fails, catch the error.

A Conceptual Model

Think of a Promise like placing an order at a restaurant. The moment you order, you receive a ticket (the Promise). The food is not ready yet, but the ticket represents the commitment that food is coming. You do not stand frozen at the counter — you go sit down and wait (the browser continues running other JavaScript). When the food is ready, the waiter brings it to your table and you eat (the .then() callback runs with the result). If the kitchen runs out of ingredients, the waiter comes to tell you there is a problem (the .catch() callback runs with the error).

Modern Syntax: async and await

A newer way to write the same logic uses the async and await keywords. You may see this style in documentation and other codebases.

async function loadResources() {
  try {
    const response = await fetch("data/resources.json");
    const data = await response.json();
    renderResources(data);
  } catch (error) {
    showError(error);
  }
}

loadResources();

async before a function declaration marks the function as asynchronous. Inside an async function, await pauses execution of that function — not the entire page — until the Promise resolves, and then returns the resolved value. try/catch handles errors in place of .catch().

Which Style Should You Use?

Both the .then()/.catch() style and the async/await style accomplish the same thing. This textbook uses the .then()/.catch() style in primary examples, since it makes the Promise structure more visible, but shows the async/await style as well so you can read it when you encounter it elsewhere.

Sections 28.7 – 28.10
From Data to Page: Rendering, States, and Responsibility
Rendering HTML · Loading and Error States · Responsible Data Use

Rendering Data on a Page

Loading data and logging it to the console is only the beginning. The real goal is to display that data as visible HTML on the page. The process follows a clear pattern:

The Render Pattern
  • Step 1Fetch the data.
  • Step 2Parse the JSON.
  • Step 3Loop through the array.
  • Step 4For each item, build an HTML string or create DOM elements.
  • Step 5Insert the built HTML into the page.

Preparing the HTML

Create a container element in your HTML where the rendered content will appear.

<section id="resources-section">
  <h2>Web Development Resources</h2>
  <div id="resources-list"></div>
</section>

The <div id="resources-list"> is intentionally empty. JavaScript will fill it.

Building and Inserting HTML with a Loop

In your JavaScript file, write a function that receives the data array and renders it into the container.

function renderResources(resources) {
  const container = document.querySelector("#resources-list");

  let html = "";

  for (let i = 0; i < resources.length; i++) {
    const resource = resources[i];
    html += `
      <div class="resource-card">
        <h3>${resource.title}</h3>
        <span class="category">${resource.category}</span>
        <p>${resource.description}</p>
        <a href="${resource.url}" target="_blank" rel="noopener noreferrer">
          Visit Resource
        </a>
      </div>
    `;
  }

  container.innerHTML = html;
}
Reading the Function
  • Selectconst container = document.querySelector("#resources-list") selects the empty container div.
  • Startlet html = "" starts with an empty string that will accumulate HTML markup.
  • LoopThe for loop runs once for each resource in the array. resources[i] gives access to the current resource object.
  • BuildInside the loop, a template literal builds an HTML card for the current resource. Each property is inserted using the ${} syntax.
  • Insertcontainer.innerHTML = html replaces the container's content with the assembled HTML string.

Connecting the Fetch to the Render Function

fetch("data/resources.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    renderResources(data);
  })
  .catch(function(error) {
    console.log("Could not load resources:", error);
  });

When the page loads, JavaScript requests the JSON file, parses it, and passes the array to renderResources(), which builds and inserts the HTML. The result is a page that displays all five resources as cards, built entirely from data.

Using forEach Instead of a for Loop

Arrays in JavaScript have a built-in forEach method that runs a function once for each item. Many developers prefer it over an index-based for loop for this kind of work.

function renderResources(resources) {
  const container = document.querySelector("#resources-list");

  let html = "";

  resources.forEach(function(resource) {
    html += `
      <div class="resource-card">
        <h3>${resource.title}</h3>
        <span class="category">${resource.category}</span>
        <p>${resource.description}</p>
        <a href="${resource.url}" target="_blank" rel="noopener noreferrer">
          Visit Resource
        </a>
      </div>
    `;
  });

  container.innerHTML = html;
}

The behavior is identical to the for loop version. resources.forEach(function(resource) { ... }) passes each item in the array to the callback function as the resource parameter.

Loading States

When a page makes a network request, there is a period of time between when the request is sent and when the data arrives. During this time, the container is empty. If a user loads the page on a slow connection, they might see a blank area for several seconds with no explanation. A good user experience shows a loading state during this period — some kind of message or visual indicator that data is on its way.

Implementing a Loading Message

Set a loading message in the container before the fetch() call begins.

const container = document.querySelector("#resources-list");

// Show loading message before the fetch begins
container.innerHTML = "<p class='loading-message'>Loading resources...</p>";

fetch("data/resources.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    renderResources(data);   // This replaces the loading message
  })
  .catch(function(error) {
    container.innerHTML = "<p class='error-message'>Could not load resources. Please try again later.</p>";
  });

The loading message is inserted before the fetch starts. When renderResources(data) runs, it replaces container.innerHTML, clearing the loading message. If an error occurs, the catch block replaces the loading message with an error message instead.

Accessible Loading Messages

Add a role="status" attribute to the loading message so that screen readers announce it:

<p class="loading-message" role="status">Loading resources...</p>
Why This Helps

You can add this attribute in JavaScript when building the loading message string. This helps users who rely on screen readers know that something is happening on the page, even though they cannot see it. Some websites show animated spinners or skeleton screens during loading — these are CSS-based techniques. For this textbook, a simple text message is appropriate; more advanced loading UI patterns belong in later front-end development courses.

Error States

Network requests can fail for many reasons: the server is unreachable, the file does not exist, the JSON is malformed, the user's connection is offline. A well-built page handles these failures gracefully instead of silently showing a blank container.

What Causes a Fetch Error

A .catch() block runs when the request itself fails — for example, a network error or a DNS failure. It does not automatically run for HTTP error responses like 404 (Not Found) or 500 (Server Error). When a server returns a 404, fetch() still considers the request a technical success and does not reject the Promise.

To handle HTTP errors properly, check the response.ok property, which is true for status codes in the 200–299 range and false for anything else.

fetch("data/resources.json")
  .then(function(response) {
    if (!response.ok) {
      throw new Error("Server returned an error: " + response.status);
    }
    return response.json();
  })
  .then(function(data) {
    renderResources(data);
  })
  .catch(function(error) {
    showError(error.message);
  });

if (!response.ok) checks whether the HTTP status code indicates success. If the server returns a 404 or 500, response.ok is false. throw new Error(...) forces the Promise chain into the rejection state, triggering the .catch() block with a descriptive message.

Showing a Helpful Error Message

function showError(message) {
  const container = document.querySelector("#resources-list");
  container.innerHTML = `
    <p class="error-message" role="alert">
      We were unable to load the resources at this time. Please try refreshing the page.
    </p>
  `;
  console.error("Fetch error:", message);
}

Note the role="alert" attribute. This tells screen readers to announce the message immediately when it appears, which is appropriate for error conditions. Log the technical error to the console for debugging purposes. Do not show raw technical error messages to users — they are rarely helpful and can sometimes expose information about your site's internal structure.

Three States of a Data-Driven Component

State 1
Loading
Data has been requested but not yet received. Show a loading message.
State 2
Success
Data arrived and was parsed correctly. Show the rendered content.
State 3
Error
Something went wrong. Show a helpful, accessible error message.

Planning for all three states is a professional habit. Pages that only handle the success case will show users blank or broken content whenever anything goes wrong.

Responsible Use of External Data

So far, this chapter has used a local JSON file. In real projects, you will often fetch data from external APIs — services provided by other organizations that return data over the web. Before doing so, there are important practices and principles to understand.

What an API Is

An API (Application Programming Interface) is a structured way for one software system to communicate with another. A web API accepts HTTP requests and returns data, usually in JSON format. For example, a weather service might offer an API endpoint at a URL like https://api.example-weather.com/forecast?city=Accra. Your JavaScript sends a request to that URL, and the weather service responds with JSON containing temperature, conditions, and forecast data.

Working with real external APIs is a topic for later courses in JavaScript and full-stack development. This chapter focuses on the patterns for reading and rendering data, which are the same whether the data comes from a local file or a remote API.

ConsiderationWhat to Do
Attribution and terms of service External data sources are owned by people and organizations. Before using data from an external source, read its terms of service. Some data is freely available for any use; some requires attribution; some prohibits commercial use; some requires an API key. For public data you use on your website, attribute the source clearly — this is good practice even when not strictly required.
Sanitizing untrusted data When you insert fetched data into the page using innerHTML, you are trusting that the data does not contain malicious HTML or scripts. For data you control, this is not a concern. For data from third-party sources, treat the content carefully and consider using textContent instead of innerHTML for plain text.
Sensitive or personal data Some APIs return personal data — names, email addresses, phone numbers. Handle this data responsibly. Do not store it beyond what is necessary, and be especially careful about logging personal data to the console in production code.
Rate limits and fair use Many external APIs enforce rate limits — restrictions on how many requests you can make in a given time period. Sending too many requests can get your IP address blocked or your API key revoked. Later courses introduce strategies like caching to reduce unnecessary calls.

Sanitizing in Practice

// Safer for plain text content that should not contain HTML
const titleElement = document.createElement("h3");
titleElement.textContent = resource.title;  // Not innerHTML

Using textContent means any HTML-like characters in the string are treated as plain text, not as markup. For beginner projects using your own controlled data, innerHTML with template literals is fine. As you work with more sources, developing the habit of distinguishing between markup you build and data you receive is important.

Building the Resource Directory

This worked example brings together every skill from this chapter. You will build a resource directory page that loads resources from a local JSON file and displays them as filterable cards.

Step 1 — Create the Project Files

web-project/ resources.html css/ main.css js/ resources.js data/ resources.json

Step 2 — Populate resources.json

[
  {
    "id": 1,
    "title": "MDN Web Docs: HTML",
    "category": "HTML",
    "description": "Mozilla's comprehensive reference for HTML elements, attributes, and best practices.",
    "url": "https://developer.mozilla.org/en-US/docs/Web/HTML"
  },
  {
    "id": 2,
    "title": "MDN Web Docs: CSS",
    "category": "CSS",
    "description": "Mozilla's complete CSS reference including properties, selectors, and layout guides.",
    "url": "https://developer.mozilla.org/en-US/docs/Web/CSS"
  },
  {
    "id": 3,
    "title": "MDN Web Docs: JavaScript",
    "category": "JavaScript",
    "description": "Mozilla's JavaScript reference covering syntax, built-in objects, and browser APIs.",
    "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript"
  },
  {
    "id": 4,
    "title": "The A11Y Project",
    "category": "Accessibility",
    "description": "A community-driven effort to make digital accessibility easier to understand and practice.",
    "url": "https://www.a11yproject.com"
  },
  {
    "id": 5,
    "title": "CSS-Tricks: Flexbox Guide",
    "category": "CSS",
    "description": "A thorough visual guide to CSS Flexbox with diagrams and interactive examples.",
    "url": "https://css-tricks.com/snippets/css/a-guide-to-flexbox/"
  },
  {
    "id": 6,
    "title": "JavaScript.info",
    "category": "JavaScript",
    "description": "A modern JavaScript tutorial covering the language from basics to advanced topics.",
    "url": "https://javascript.info"
  },
  {
    "id": 7,
    "title": "web.dev: Responsive Design",
    "category": "CSS",
    "description": "Google's guide to building responsive layouts for different screen sizes.",
    "url": "https://web.dev/learn/design"
  },
  {
    "id": 8,
    "title": "WebAIM: Color Contrast Checker",
    "category": "Accessibility",
    "description": "An online tool for checking whether text and background color combinations meet accessibility standards.",
    "url": "https://webaim.org/resources/contrastchecker/"
  }
]

Step 3 — Build resources.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Development Resources</title>
  <link rel="stylesheet" href="css/main.css">
</head>
<body>

  <header>
    <nav>
      <a href="index.html">Home</a>
      <a href="about.html">About</a>
      <a href="resources.html" aria-current="page">Resources</a>
      <a href="contact.html">Contact</a>
    </nav>
  </header>

  <main>
    <h1>Web Development Resources</h1>
    <p>A curated list of resources for learning HTML, CSS, JavaScript, and accessibility.</p>

    <div id="filter-controls" role="group" aria-label="Filter resources by category">
      <button class="filter-btn active" data-category="all">All</button>
      <button class="filter-btn" data-category="HTML">HTML</button>
      <button class="filter-btn" data-category="CSS">CSS</button>
      <button class="filter-btn" data-category="JavaScript">JavaScript</button>
      <button class="filter-btn" data-category="Accessibility">Accessibility</button>
    </div>

    <div id="resources-list" aria-live="polite"></div>
  </main>

  <footer>
    <p>&copy; 2025 Student Learning Site</p>
  </footer>

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

Note the aria-live="polite" attribute on #resources-list. This tells screen readers to announce changes to this region when they occur — useful when filtering updates the content.

Step 4 — Write resources.js

// Store all resources for filtering
let allResources = [];

// Select the container
const container = document.querySelector("#resources-list");

// Show loading state
container.innerHTML = "<p class='loading-message' role='status'>Loading resources...</p>";

// Fetch the data
fetch("data/resources.json")
  .then(function(response) {
    if (!response.ok) {
      throw new Error("Network response was not OK: " + response.status);
    }
    return response.json();
  })
  .then(function(data) {
    allResources = data;
    renderResources(allResources);
    setupFilters();
  })
  .catch(function(error) {
    container.innerHTML = `
      <p class="error-message" role="alert">
        Resources could not be loaded. Please try refreshing the page.
      </p>
    `;
    console.error("Fetch error:", error.message);
  });


// Build and insert cards for a given array of resources
function renderResources(resources) {
  if (resources.length === 0) {
    container.innerHTML = "<p class='no-results'>No resources found for this category.</p>";
    return;
  }

  let html = "";

  resources.forEach(function(resource) {
    html += `
      <div class="resource-card">
        <div class="card-header">
          <h2>${resource.title}</h2>
          <span class="category-badge">${resource.category}</span>
        </div>
        <p class="card-description">${resource.description}</p>
        <a class="card-link" href="${resource.url}" target="_blank" rel="noopener noreferrer">
          Visit Resource <span aria-hidden="true">→</span>
        </a>
      </div>
    `;
  });

  container.innerHTML = html;
}


// Set up filter buttons
function setupFilters() {
  const buttons = document.querySelectorAll(".filter-btn");

  buttons.forEach(function(button) {
    button.addEventListener("click", function() {
      // Update active button styling
      buttons.forEach(function(btn) {
        btn.classList.remove("active");
        btn.removeAttribute("aria-pressed");
      });
      button.classList.add("active");
      button.setAttribute("aria-pressed", "true");

      // Filter and render
      const category = button.getAttribute("data-category");
      if (category === "all") {
        renderResources(allResources);
      } else {
        const filtered = allResources.filter(function(resource) {
          return resource.category === category;
        });
        renderResources(filtered);
      }
    });
  });
}

Step 5 — Add CSS

In css/main.css, add styles for the resource cards and filter buttons:

#filter-controls {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 2rem;
}

.filter-btn {
  padding: 0.5rem 1rem;
  border: 2px solid var(--color-primary);
  background: transparent;
  color: var(--color-primary);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
}

.filter-btn.active,
.filter-btn:hover {
  background: var(--color-primary);
  color: white;
}

#resources-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.5rem;
}

.resource-card {
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.5rem;
}

.card-header h2 {
  font-size: 1rem;
  margin: 0;
}

.category-badge {
  font-size: 0.75rem;
  padding: 0.2rem 0.6rem;
  background: var(--color-accent-light);
  color: var(--color-accent);
  border-radius: 12px;
  white-space: nowrap;
}

.card-description {
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--color-text-secondary);
}

.card-link {
  margin-top: auto;
  font-size: 0.9rem;
  font-weight: 600;
}

.loading-message,
.error-message,
.no-results {
  padding: 2rem;
  text-align: center;
  font-size: 1rem;
}

.error-message {
  color: var(--color-error);
}
What This Example Demonstrates

This worked example uses every major concept from the chapter: a local JSON file with an array of objects, fetch() with response.ok checking, a loading state before the fetch, an error state in the catch block, a renderResources() function that builds HTML with a forEach loop, a filter system that uses Array.filter() to select a subset of the loaded data, and accessibility attributes on interactive regions and buttons.

Chapter Summary

Key Terms

API
Application Programming Interface. A structured way for software systems to communicate. Web APIs accept HTTP requests and return data, usually in JSON.
array of objects
A JSON or JavaScript array where each item is an object with properties. The most common structure for lists of similar content items.
asynchronous
Describes operations that do not block the rest of a program while waiting. Network requests in JavaScript are asynchronous.
fetch()
A built-in browser function that sends an HTTP request from JavaScript and returns a Promise.
JSON
JavaScript Object Notation. A text-based format for structured data using objects and arrays.
loading state
The visual and structural state of a page component while data is being fetched.
error state
The visual and structural state of a page component when a data fetch has failed.
Promise
A JavaScript object that represents the eventual result of an asynchronous operation. Settled through .then() (success) and .catch() (failure).
rate limit
A restriction imposed by an API provider on how many requests can be made within a given time period.
response.json()
A method on the Response object returned by fetch(). It parses the response body as JSON and returns a Promise that resolves to the parsed value.
response.ok
A Boolean property of the Response object. true if the HTTP status code is in the 200–299 range; false otherwise.
sanitizing
Treating user or external data as potentially unsafe, and handling it so it cannot be misinterpreted as executable code.
textContent
A DOM property that sets or reads the plain text content of an element, treating any HTML characters as literal text rather than markup.

Concept Check

Answer these questions before beginning the full review questions. They check basic understanding, not deep analysis.

  1. In your own words, why do many modern websites separate content from the HTML that displays it?
  2. What is JSON, and what are two rules that make it stricter than a JavaScript object literal?
  3. What is the difference between a JSON object and a JSON array? What is an "array of objects"?
  4. Why must a local JSON file be served by a development server before fetch() can load it?
  5. What does it mean for fetch() to be asynchronous?
  6. What are the three states of a Promise?
  7. Name the three states a data-driven page component should plan for, and describe what the user sees in each.

Code Reading

Read each code block carefully before answering the questions that follow it.

Code Block A
[
  {
    "id": 1,
    "name": "Intro to HTML",
    "type": "lesson",
    "completed": true
  },
  {
    "id": 2,
    "name": "CSS Box Model",
    "type": "lesson",
    "completed": false
  },
  {
    "id": 3,
    "name": "HTML Practice Quiz",
    "type": "quiz",
    "completed": true
  }
]
  1. Is the top-level structure an object or an array?
  2. How many items are in this structure?
  3. What are the property names shared by all items?
  4. What type is the completed property?
  5. Is this valid JSON? Identify any issues, or confirm that it is valid.
Code Block B
fetch("data/lessons.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log(data.length);
  })
  .catch(function(error) {
    console.log("Error:", error);
  });
  1. What file is being fetched?
  2. What does response.json() do?
  3. What does data.length represent, assuming the JSON contains the array from Code Block A?
  4. What would be logged to the console for the example data?
  5. Under what conditions would the .catch() block run?
Code Block C
function renderLessons(lessons) {
  const list = document.querySelector("#lesson-list");
  let html = "";

  lessons.forEach(function(lesson) {
    html += `<li class="${lesson.completed ? 'done' : 'pending'}">${lesson.name}</li>`;
  });

  list.innerHTML = "<ul>" + html + "</ul>";
}
  1. What argument does renderLessons expect?
  2. What does forEach do here?
  3. What is the ? : operator doing in the template literal?
  4. What HTML would be produced for the array in Code Block A?
  5. What would the page show if lessons were an empty array?

Code Tracing

Trace through the following code manually. At each step, write what the current value of the variable or expression is.

const resources = [
  { id: 1, title: "MDN HTML", category: "HTML" },
  { id: 2, title: "MDN CSS", category: "CSS" },
  { id: 3, title: "MDN JS", category: "JavaScript" },
  { id: 4, title: "A11Y Project", category: "Accessibility" },
  { id: 5, title: "CSS-Tricks Flexbox", category: "CSS" }
];

const cssOnly = resources.filter(function(item) {
  return item.category === "CSS";
});

console.log(cssOnly.length);
console.log(cssOnly[0].title);
console.log(cssOnly[1].title);
  1. After the filter call, what does cssOnly contain? Write out its contents.
  2. What is logged by console.log(cssOnly.length)?
  3. What is logged by console.log(cssOnly[0].title)?
  4. What is logged by console.log(cssOnly[1].title)?

Predict the Result

For each scenario, predict what the user will see or what will happen.

  1. A page uses fetch() to load data/items.json. The file does not exist on the server, and the server returns a 404 status code. The JavaScript checks response.ok and throws an error. What does the user see if the catch block runs container.innerHTML = "<p class='error-message'>Could not load items.</p>"?
  2. A page sets container.innerHTML = "<p>Loading...</p>" before the fetch() call. The fetch succeeds and renderItems(data) runs, setting container.innerHTML to a list of cards. What does the user see during loading, and what do they see after loading completes?
  3. A renderItems function receives an empty array []. Inside, forEach runs but the loop body never executes. The function sets container.innerHTML = html, where html was initialized as an empty string. What does the container look like after the function runs? How could you improve this?
  4. A JSON file contains the following. Will it parse successfully?
    {
      "name": "Lesson One",
      "order": 1,
      "tags": ["beginner", "html"],
    }

Common Misconceptions

Misconception 1
"JSON and JavaScript objects are exactly the same thing."
JSON is a text-based data format that looks similar to a JavaScript object, but it has stricter rules: property names must be double-quoted, trailing commas are not allowed, comments are not allowed, and functions cannot appear as values. JSON text must be parsed — for example with response.json() or JSON.parse() — before it becomes an actual JavaScript array or object you can work with.
Misconception 2
"If a server returns an error page, like a 404, my .catch() block will run automatically."
fetch() only rejects its Promise — triggering .catch() — when the request itself fails to complete, such as a network outage. A 404 or 500 response is still a completed HTTP exchange, so fetch() treats it as fulfilled. You must check response.ok yourself and throw an error if it is false.
Misconception 3
"Because fetch() is just a function call, the data is available on the very next line of code."
fetch() is asynchronous. It returns a Promise immediately, but the actual response arrives later, whenever the network request completes. Code that needs the data must go inside a .then() callback (or after an await), not on the line directly following the fetch() call.
Misconception 4
"Sanitizing data only matters for professional developers working with sensitive systems — not for a small beginner project."
For your own JSON files that you write and control, the risk is minimal. But the habit of treating external or user-supplied content carefully — for example, using textContent for plain text instead of innerHTML — is worth building early. The moment a beginner project starts pulling in data from an outside source, that habit becomes a real protection rather than an abstract rule.

Debugging and Error Analysis

Each item below contains an error or problem. Identify it and describe how to fix it.

Debugging Task 1
{
  'title': 'Intro to CSS',
  'chapter': 11,
  'published': true
}
Debugging Task 2
fetch("data/resources.json")
  .then(function(response) {
    return response.json;
  })
  .then(function(data) {
    renderResources(data);
  });
Debugging Task 3
fetch("data/resources.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    renderResources(data);
  });

function renderResources(resources) {
  const container = document.querySelector("#resource-list");
  resources.forEach(function(resource) {
    container.innerHTML += `<div>${resource.tittle}</div>`;
  });
}
Debugging Task 4
A student writes the following. The page opens but the container stays blank with no loading message and no cards. What is likely wrong?
fetch("data/items.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    renderItems(data);
  });
The HTML is:
<div id="items-container"></div>
The JavaScript renderItems function:
function renderItems(items) {
  const container = document.querySelector("#item-container");
  let html = "";
  items.forEach(function(item) {
    html += `<p>${item.name}</p>`;
  });
  container.innerHTML = html;
}
PracticeShort Build Tasks
  1. Team JSON File. Create a file called team.json in a data/ folder. It should contain an array of at least four team member objects. Each object should have id, name, role, and bio properties. Make sure the JSON is valid.
  2. Render Function. Write a renderTeam(members) function that builds an HTML card for each member and inserts them into a <div id="team-grid">. Each card should display the member's name, role, and a short excerpt from their bio.
  3. Complete Fetch Call. Write a complete fetch() call that loads data/team.json, handles the response, calls renderTeam(data) on success, and displays an error message in #team-grid if the fetch fails.
  4. Loading Message. Add a loading message that appears in #team-grid before the fetch begins, and is replaced by either the team cards or an error message when the fetch completes.
  5. Filtering Events. A page displays events loaded from JSON. Add a filter that shows only events where event.type === "workshop". The filtered array should be passed to the render function. The unfiltered array should be stored so a "Show All" button can restore it.
Mini-ProjectJSON Resource Directory

Build a complete, working resource directory page using a local JSON file.

Requirements

JSON file: Create data/resources.json with at least eight resource objects. Each resource must have id, title, category, description, and url properties. Include at least three different category values.

HTML page: resources.html with a proper page structure including <header>, <main>, and <footer>, a heading and introductory paragraph, a row of filter buttons (one per category plus an "All" button), an empty container div with an appropriate id, and the JavaScript file linked before </body>.

JavaScript: Load the JSON with fetch(), display a loading message before the request, handle response.ok and throw an error for non-success responses, render the resources as cards in the container, show an error message if the fetch fails, implement filter buttons that re-render a subset of the data, and keep the full data array accessible so the "All" button restores the complete list.

CSS: Style the cards in a responsive grid using CSS Grid or Flexbox, style the filter buttons with a clear active state, style the loading and error messages appropriately, and make the layout responsive so cards stack properly on small screens.

Accessibility: Include aria-live="polite" on the resources container, use role="status" on the loading message, use role="alert" on the error message, use aria-pressed on filter buttons to indicate the active state, and ensure all links have meaningful text.

Extension Tasks (Optional)

  • Add a text search input that filters resources by title or description as the user types.
  • Add a count display that shows how many resources are currently visible (for example, "Showing 3 of 8 resources").
  • Sort resources alphabetically by title within each category.

Accessibility Check

Review the worked example from this chapter and answer the following questions.

  1. The #resources-list container has aria-live="polite". What does this do? When would aria-live="assertive" be more appropriate?
  2. The loading message uses role="status". What kind of user benefits from this attribute?
  3. The error message uses role="alert". How does this differ from role="status" in terms of how screen readers handle the announcement?
  4. The filter buttons use aria-pressed="true" on the active button. What information does this communicate to assistive technology users?
  5. The external links include rel="noopener noreferrer". What does this attribute do, and why is it a good practice?

Mixed Review

These questions draw on skills from earlier chapters.

  1. (Chapter 3) If your HTML page is at resources.html and your JSON file is at data/resources.json, write the correct path to use in your fetch() call.
  2. (Chapter 9) A resource card contains an external link. Write the complete <a> tag for a link that opens in a new tab and includes rel="noopener noreferrer".
  3. (Chapter 16) Write the CSS needed to display a container as a responsive grid with a minimum column width of 260px, using repeat(auto-fill, minmax(260px, 1fr)).
  4. (Chapter 22) Write the JavaScript to select the element with id="resources-list" and set its text content to "Loading...".
  5. (Chapter 23) Given an array called allItems, write a line of JavaScript that creates a new array called completedItems containing only items where item.completed === true.
  6. (Chapter 24) Explain in one or two sentences why front-end filtering of fetched data does not replace server-side filtering for large datasets or sensitive data.
  7. (Chapter 27) The filter buttons in the worked example toggle an .active class. In the setupFilters function, what does the line buttons.forEach(function(btn) { btn.classList.remove("active"); }) accomplish, and why does it run before adding the .active class to the clicked button?
Chapter 28 Checklist
Use this to confirm your understanding before moving to Chapter 29
Concepts
  • I can explain why websites separate data from presentation.
  • I can write valid JSON, including objects, arrays, and arrays of objects.
  • I can identify common JSON syntax errors, such as single quotes and trailing commas.
  • I can explain why fetch() needs a local development server to load local files.
  • I can explain why network requests in JavaScript are asynchronous.
  • I can describe the three states of a Promise and how .then() and .catch() work.
Applications
  • I can write a fetch() call that loads a local JSON file and renders it as HTML.
  • I can implement a loading state and an error state for a data-driven page component.
  • I can check response.ok and throw an error for a non-success HTTP status.
  • I can explain at least two responsible practices for using external data or APIs.
Study Habits
  • I traced the Promise chain examples line by line rather than skimming them.
  • I can read an unfamiliar JSON structure and describe its shape before writing code for it.
  • I understand the difference between innerHTML and textContent and when to prefer each.
Preview — Chapter 29
Accessibility, Performance, Testing, and Debugging

Chapter 28 taught you how to separate content from code and bring it to life with fetch(), Promises, and rendered HTML. Chapter 29 consolidates the quality practices introduced throughout this textbook into a comprehensive review and audit process. You will learn systematic approaches to checking semantic structure, keyboard navigation, color contrast, alt text, form labels, image optimization, HTML validation, CSS debugging, JavaScript debugging, browser testing, and quality checklists. Chapter 29 treats accessibility and performance not as finishing touches but as fundamental requirements of any public website — and the skills in that chapter apply directly to the data-driven resource directory you built in this chapter.

MGU Library Connection
  • Web Development: HTML, CSS, and JavaScript — Appendix I: JSON and Fetch Reference
  • Web Development: HTML, CSS, and JavaScript — Appendix M: Glossary
  • Companion resource: JSON Quick Reference
  • Companion resource: Fetch Beginner Guide
  • Next chapter: Chapter 29 — Accessibility, Performance, Testing, and Debugging
  • MGU Digital Literacy — background reading on data privacy and responsible technology use