Level 4, Real Data & Architecture, Chapter 15 of 24
Working with APIs & JSON
Why this matters
Almost nothing on the modern web is written by hand into an HTML file. Weather apps ask a weather service for forecasts. Shopping sites ask a product database for prices and stock. Social apps ask a server for the latest posts. All of this communication happens through APIs, and the data that flows through them is almost always formatted as JSON.
Once you know how to fetch data from an API and turn it into visible, interactive HTML, you stop being limited to information you type in yourself. Your pages can react to the real world: live prices, current weather, search results, or your own backend. This chapter is the bridge between building static pages and building real applications.
The lesson
What JSON actually is
JSON, short for JavaScript Object Notation, is a text format for representing data that looks almost identical to JavaScript object and array literals, but with stricter rules: property names must be in double quotes, there are no trailing commas, and it can't contain functions or comments. It became the standard format for web APIs because it's lightweight and maps directly onto the objects and arrays you already know. When your JavaScript receives JSON text from a server, you convert it into a real JavaScript object with JSON.parse(). When you need to send JavaScript data to a server, you convert it into JSON text with JSON.stringify().
1{2 "id": 42,3 "title": "Learn APIs",4 "tags": ["javascript", "web"],5 "completed": false6}Fetching data
The fetch() function is built into the browser and lets you request data from any URL. Calling fetch(url) returns a Promise, which resolves to a Response object once the network request completes. That Response has a .json() method (itself returning another Promise) that parses the response body as JSON. Because fetch involves waiting for a network round trip, you'll usually pair it with async/await, which the next chapter covers in depth, but you can get productive with the simpler .then() chain right away.
1fetch("https://jsonplaceholder.typicode.com/users")2 .then((response) => response.json())3 .then((users) => {4 const names = users.map((u) => u.name);5 console.log(names);6 })7 .catch((error) => console.error("Request failed:", error));Notice the .catch() at the end. Network requests fail for all sorts of reasons: no internet connection, a slow server, a typo in the URL, or the API being temporarily down. Unlike a normal JavaScript error, fetch will not automatically throw for HTTP error responses like 404 or 500, it only rejects the promise for network-level failures. That means good fetch code checks response.ok before trying to parse the body, and throws its own error if the request didn't succeed, so your .catch() can handle it gracefully instead of your page silently breaking.
1fetch("https://jsonplaceholder.typicode.com/users/9999")2 .then((response) => {3 if (!response.ok) {4 throw new Error(`Request failed with status ${response.status}`);5 }6 return response.json();7 })8 .then((user) => console.log(user))9 .catch((error) => console.error(error.message));Rendering fetched data into the DOM
Fetching data is only half the job, you also need to turn it into HTML the user can see, using the DOM skills from earlier chapters. A reliable pattern is to build up a string of HTML using template literals and array map, then set it on a container element's innerHTML in one step, rather than creating and appending many elements one at a time. This keeps your rendering code short and easy to follow, though for larger apps you'll eventually want to be careful about inserting raw user-provided text this way, since unescaped HTML can introduce security issues.
fetch(url)starts a network request and returns a Promise for a Responseresponse.json()parses the response body and returns a Promise for the data- Always check
response.okbefore assuming the request succeeded JSON.stringify(obj)turns a JavaScript value into JSON text to send- Wrap fetch chains in
.catch()so network failures don't crash silently - Render fetched data by mapping it into an HTML string and setting innerHTML
It's worth practicing with a real, free API that requires no signup, like JSONPlaceholder, which simulates a typical REST API with users, posts, and comments. Try fetching a list, rendering it, then fetching details for a single item when the user clicks something. This click-to-fetch-more pattern, sometimes called lazy loading, is extremely common: think of a product listing page that only loads full details when you click into a specific item.
Try it yourself
This sandbox simulates an API using a local data object and a fake fetch-like delay, so it works offline. Click the button to load and render a list of articles.
Chapter boss project
Build a Live Search Directory
Build a small directory app that fetches a list of items from a public API (or the simulated data object approach if you prefer to work offline) and lets the user filter results by typing into a search box.
The list should render as cards showing at least two pieces of information per item. As the user types, the visible list should filter down using the array methods from the previous chapter, without re-fetching data on every keystroke.
Handle the loading and error states visibly: show a loading message while the request is in flight, and show a friendly error message if the request fails, rather than leaving the page blank.
- Fetch the data once when the page loads and store it in a variable, then filter that stored array locally as the user types.
- Use the `input` event on your search box, and filter using `.toLowerCase().includes()` for case-insensitive matching.
- Structure your render function so it takes an array and produces HTML, so you can call the same function for the full list and the filtered list.
- Test your error handling by temporarily changing the URL to something invalid to confirm your catch block runs.
Level-up checklist
Tick these off once each one is true for you.
