How fetch() works
You ask a URL for data, wait for it to arrive, turn it into a JavaScript object, then put it on the page. Because it takes time, you use async/await and always wrap it in try/catch.
async function loadData() {
try {
const res = await fetch('https://dummyjson.com/quotes/random'); // 1. ask the URL
if (!res.ok) throw new Error('HTTP ' + res.status); // 2. check it worked
const data = await res.json(); // 3. parse JSON → object
render(data); // 4. show it
} catch (err) {
showError(err.message); // network/parse failed
}
}
Fetch a real quote
This actually calls a public API over the internet when you click. Watch the loading spinner, then the result — or an error message if the network is down.
Click the button to fetch a random quote.
<button class="btn" id="quoteBtn">Get a quote</button> <div id="quoteOut"> <div class="quote-box"><p>Click the button to fetch a random quote.</p></div> </div>
.btn { border: none; background: #0ea5e9; color: #fff; font-weight: 700; padding: 11px 20px; border-radius: 9px; cursor: pointer; }
.quote-box { background: #f0f9ff; border-left: 4px solid #0ea5e9; border-radius: 0 10px 10px 0; padding: 16px 18px; margin-top: 14px; }
.quote-box p { font-style: italic; color: #0c4a6e; }
.quote-box cite { display: block; margin-top: 8px; font-style: normal; color: #0369a1; font-weight: 700; font-size: .82rem; }
.spinner { width: 30px; height: 30px; border: 4px solid #e5e7eb; border-top-color: #0ea5e9; border-radius: 50%; animation: spin .8s linear infinite; margin: 14px 0; }
@keyframes spin { to { transform: rotate(360deg); } }
.error { color: #b91c1c; }
const btn = document.getElementById('quoteBtn');
const out = document.getElementById('quoteOut');
btn.addEventListener('click', async () => {
out.innerHTML = '<div class="spinner"></div>'; // loading state
try {
const res = await fetch('https://dummyjson.com/quotes/random');
if (!res.ok) throw new Error('Request failed');
const q = await res.json();
out.innerHTML = `<div class="quote-box"><p>"${q.quote}"</p><cite>— ${q.author}</cite></div>`; // success
} catch (e) {
out.innerHTML = '<p class="error">Could not load. Try again.</p>'; // error state
}
});
Render a list — and handle every state
Real requests don't always go smoothly. A good fetch handles loading, success, empty (it worked but there's no data), and error. Try each below (these are simulated so you can see all four reliably):
<button class="btn" id="loadOk">Load users</button> <div id="usersOut"><div class="state">Press the button above.</div></div>
.btn { border: none; background: #0ea5e9; color: #fff; font-weight: 700; padding: 11px 20px; border-radius: 9px; cursor: pointer; }
.ulist { list-style: none; display: grid; gap: 8px; padding: 0; margin-top: 14px; }
.ucard { display: flex; align-items: center; gap: 12px; background: #f6f7f9; border-radius: 10px; padding: 10px 14px; }
.ucard .av { width: 38px; height: 38px; border-radius: 50%; background: linear-gradient(135deg,#0ea5e9,#8b5cf6); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }
.ucard .nm { font-weight: 600; color: #111; font-size: .9rem; }
.ucard .em { color: #6b7280; font-size: .78rem; }
.state { text-align: center; color: #6b7280; padding: 18px; }
.state.empty { color: #9ca3af; } .state.error { color: #b91c1c; }
.spinner { width: 30px; height: 30px; border: 4px solid #e5e7eb; border-top-color: #0ea5e9; border-radius: 50%; animation: spin .8s linear infinite; margin: 14px auto; }
@keyframes spin { to { transform: rotate(360deg); } }
const out = document.getElementById('usersOut');
// your real fetch — here we pull sample users from a free, no-key API
async function getUsers() {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
}
async function loadUsers() {
out.innerHTML = '<div class="spinner"></div>'; // 1. LOADING
try {
const users = await getUsers();
if (users.length === 0) { // 2. EMPTY
out.innerHTML = '<div class="state empty">No users yet.</div>';
return;
}
out.innerHTML = '<ul class="ulist">' + users.map(u => // 3. SUCCESS — map data → HTML
`<li class="ucard"><span class="av">${u.name[0]}</span>
<span><span class="nm">${u.name}</span><br><span class="em">${u.email}</span></span></li>`).join('') + '</ul>';
} catch (e) {
out.innerHTML = '<div class="state error">Something went wrong.</div>'; // 4. ERROR
}
}
document.getElementById('loadOk').addEventListener('click', loadUsers);
Free, no-key public APIs
All of these are free, need no API key, and allow browser requests (CORS) — perfect for a capstone. Just fetch() the URL.
Random quotes
dummyjson.com/quotes/randomInspirational quotes with author.
Advice
api.adviceslip.com/adviceA random piece of advice.
Cat facts
catfact.ninja/factA random fact about cats.
Countries
restcountries.com/v3.1/allFlags, capitals, population, region.
Weather
api.open-meteo.com/v1/forecastLive forecast by lat/long.
Recipes
themealdb.com/api/json/v1/1/random.phpA random meal with ingredients.
GitHub users
api.github.com/users/octocatPublic profile data & avatars.
Fake data
jsonplaceholder.typicode.com/postsDummy posts/users for practice.
fetch() plus the list-rendering pattern above. Combine with widgets from Interactive Features (search/filter the results!).The takeaway
fetch(url) → await res.json() → put it on the page. Always use
async/await inside try/catch, and always design the four states:
loading, success, empty, and error. Master this one pattern and you can pull any live data into a
capstone.