Live Data on Your Page
Open any weather app and it shows the temperature in your city right now. Open a news site and the latest stories appear without anyone editing the page by hand. This data is not written into the HTML. The page asks a server for it while it runs, and shows whatever comes back.
In JavaScript, the tool for asking is fetch(). In this lesson you will show the current weather for New Delhi, Mumbai, Bengaluru, Chennai or Kolkata, and load a list of posts from a free practice service. Along the way you will learn how to handle slow networks, errors and sending your own data.
You need an internet connection for every example in this lesson. If you are offline, the examples show their error messages instead, which is also a good test of your error handling.
This lesson uses async and await from the JavaScript async and await lesson, and JSON from the JavaScript JSON lesson.
What Is an API?
An API (Application Programming Interface) is a set of addresses on a server that programs can call to get or send data. Think of a railway enquiry counter. You do not walk into the control room. You ask the counter a question in a fixed format, “status of train number so and so”, and you get a short answer. A web API works the same way: you request a URL, and the server replies with data, usually in JSON format.
We will use two free APIs that need no account or key:
| API | What it gives |
|---|---|
jsonplaceholder.typicode.com | Fake posts, users and comments for practice. It accepts POST requests but does not really save them. |
api.open-meteo.com | Real weather data for any latitude and longitude. |
Your First GET Request
A GET request asks for data. It is what fetch(url) does by default. fetch returns a promise of a Response object. The response arrives in two parts: first the status and headers, then the body. That is why you need two awaits: one for fetch and one for response.json(), which reads the body and parses it from JSON into a JavaScript object.
// Needs an internet connection
async function getFirstPost() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
console.log(response.status); // 200
console.log(response.ok); // true
const post = await response.json(); // read the body and parse the JSON
console.log(post.id); // 1
console.log(post.title); // sunt aut facere repellat provident occaecati excepturi optio reprehenderit
}
getFirstPost();
response.ok and Status Codes
Every HTTP response has a status code, a number that says how the request went:
| Code | Meaning |
|---|---|
| 200 | OK, here is your data |
| 201 | Created, your new item was saved |
| 400 | Bad request, something in your request is wrong |
| 404 | Not found, nothing exists at that address |
| 500 | Server error, the problem is on the server |
Here is the surprise: fetch does not reject on 404 or 500. It only rejects when the request could not happen at all, for example when you are offline. A 404 is still a successful conversation with the server, just with bad news. So you must check response.ok, which is true for codes 200 to 299, and throw your own error when it is false. A small helper like getJson saves you from repeating this check:
// Needs an internet connection
async function getJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Request failed with status " + response.status);
}
return response.json();
}
async function main() {
try {
const post = await getJson("https://jsonplaceholder.typicode.com/posts/2");
console.log("Found:", post.title); // Found: qui est esse
await getJson("https://jsonplaceholder.typicode.com/posts/99999");
console.log("This line never runs");
} catch (error) {
console.log(error.message); // Request failed with status 404
}
}
main();
Query Parameters with URLSearchParams
Many APIs read options from the query string, the part of the URL after ?, written as key=value pairs joined by &. You could build it by joining strings, but special characters like spaces, commas and & must be encoded. URLSearchParams does the encoding for you. Here it builds the weather URL for New Delhi:
// Needs an internet connection
async function getWeather(latitude, longitude) {
const params = new URLSearchParams({
latitude: latitude,
longitude: longitude,
current: "temperature_2m,wind_speed_10m",
});
const url = "https://api.open-meteo.com/v1/forecast?" + params.toString();
console.log(url);
// https://api.open-meteo.com/v1/forecast?latitude=28.61&longitude=77.21¤t=temperature_2m%2Cwind_speed_10m
const response = await fetch(url);
if (!response.ok) {
throw new Error("Weather service error: " + response.status);
}
const data = await response.json();
return data.current;
}
getWeather(28.61, 77.21)
.then((now) => {
console.log("New Delhi temperature:", now.temperature_2m, "C"); // for example 31.4 C
console.log("Wind speed:", now.wind_speed_10m, "km/h"); // for example 9.2 km/h
})
.catch((error) => console.log(error.message));
The comma became %2C, its encoded form, and the server understands it. To show another city, change the latitude and longitude: Mumbai is 19.08, 72.88, Bengaluru is 12.97, 77.59, Chennai is 13.08, 80.27 and Kolkata is 22.57, 88.36. Your temperature will be different, because the data is live.
Loading and Error States on the Page
A network request can take a second or two on a slow mobile connection, and sometimes it fails. A good page always shows one of three states: loading, success or error. Never leave the user staring at an empty box. This page loads five posts. The _limit parameter is a feature of this practice API:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Latest Posts</title>
<style>
body { font-family: sans-serif; padding: 16px; }
.post { border-bottom: 1px solid #ddd; padding: 8px 0; }
.post h3 { margin: 0 0 4px; font-size: 16px; text-transform: capitalize; }
.error { color: crimson; }
</style>
</head>
<body>
<h2>Latest Posts</h2>
<p id="status">Loading posts...</p>
<div id="posts"></div>
<script>
// Needs an internet connection
const statusText = document.querySelector("#status");
const list = document.querySelector("#posts");
async function loadPosts() {
try {
const params = new URLSearchParams({ _limit: 5 });
const response = await fetch("https://jsonplaceholder.typicode.com/posts?" + params);
if (!response.ok) {
throw new Error("Server replied with status " + response.status);
}
const posts = await response.json();
posts.forEach((post) => {
const div = document.createElement("div");
div.className = "post";
const title = document.createElement("h3");
title.textContent = post.id + ". " + post.title;
const body = document.createElement("p");
body.textContent = post.body;
div.append(title, body);
list.append(div);
});
statusText.textContent = "Showing " + posts.length + " posts";
} catch (error) {
statusText.className = "error";
statusText.textContent = "Could not load posts. Check your internet connection. (" + error.message + ")";
}
}
loadPosts();
</script>
</body>
</html>
Notice the code uses textContent to put the data on the page, not innerHTML. Data from a server should be treated as plain text, so that any HTML hidden inside it cannot run on your page.
Sending JSON with POST
A POST request sends data to the server, for example a new post, a form or an order. You pass a second argument to fetch with three things:
method: "POST"to say what kind of request this is.headers: extra information about the request.Content-Type: application/jsontells the server the body is JSON.body: the data itself, turned into a JSON string withJSON.stringify().
// Needs an internet connection
async function createPost() {
const newPost = {
title: "College fest registrations are open",
body: "Register for coding, quiz and dance events before Friday.",
userId: 1,
};
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(newPost),
});
console.log(response.status); // 201 (Created)
const saved = await response.json();
console.log(saved.id); // 101 (a fake id from the practice API)
console.log(saved.title); // College fest registrations are open
}
createPost().catch((error) => console.log("Failed:", error.message));
The practice API replies with status 201 and your post plus a new id of 101. It does not really save anything, so fetching post 101 later gives a 404. A real API would store it.
Timeouts with AbortController
By default, a fetch can wait a very long time on a bad network. An AbortController lets you cancel a request. You pass its signal to fetch, and call controller.abort() when you want to give up. The fetch promise then rejects with an error named AbortError.
// Needs an internet connection
async function fetchWithTimeout(url, ms) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timer);
}
}
async function main() {
try {
const users = await fetchWithTimeout("https://jsonplaceholder.typicode.com/users", 5000);
console.log("Loaded", users.length, "users"); // Loaded 10 users
await fetchWithTimeout("https://jsonplaceholder.typicode.com/photos", 1);
} catch (error) {
console.log(error.name); // AbortError (1 ms was far too short)
}
}
main();
Modern browsers also offer a shortcut, fetch(url, { signal: AbortSignal.timeout(5000) }), which does the same thing in one line. Aborting is also useful in search boxes, to cancel an old request when the user types again.
A Word About CORS
Browsers protect users with a rule called the same-origin policy: a page from one website cannot read responses from another website unless that other website allows it. The server gives permission by sending special headers, a system called CORS (Cross-Origin Resource Sharing). Both APIs in this lesson send these headers, so they work from any page. If you see a console error mentioning CORS with some other API, your code is usually fine: that server simply does not allow browsers on other sites to read it, and you would need to call it from your own server instead.
Real-Life Example: A City Weather Card
Here is a small weather app for five Indian cities. Choosing a city or pressing Refresh shows a loading message, fetches live data, and fills a card. If anything goes wrong, the card shows a clear error. The units, such as °C and km/h, come from the API response itself.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>City Weather</title>
<style>
body { font-family: sans-serif; padding: 16px; max-width: 360px; }
select, button { padding: 8px; font-size: 16px; }
.card { margin-top: 16px; padding: 16px; border-radius: 12px; background: #eef6ff; }
.temp { font-size: 40px; font-weight: bold; margin: 4px 0; }
.error { color: crimson; }
</style>
</head>
<body>
<h2>Weather Right Now</h2>
<select id="city">
<option value="28.61,77.21">New Delhi</option>
<option value="19.08,72.88">Mumbai</option>
<option value="12.97,77.59">Bengaluru</option>
<option value="13.08,80.27">Chennai</option>
<option value="22.57,88.36">Kolkata</option>
</select>
<button id="refresh">Refresh</button>
<div class="card" id="card">Choose a city.</div>
<script>
// Needs an internet connection
const select = document.querySelector("#city");
const card = document.querySelector("#card");
const button = document.querySelector("#refresh");
async function showWeather() {
const [latitude, longitude] = select.value.split(",");
const cityName = select.options[select.selectedIndex].text;
card.textContent = "Loading weather for " + cityName + "...";
button.disabled = true;
try {
const params = new URLSearchParams({
latitude: latitude,
longitude: longitude,
current: "temperature_2m,wind_speed_10m",
});
const response = await fetch("https://api.open-meteo.com/v1/forecast?" + params);
if (!response.ok) {
throw new Error("status " + response.status);
}
const data = await response.json();
const now = data.current;
const units = data.current_units;
card.innerHTML = "";
const heading = document.createElement("strong");
heading.textContent = cityName;
const temp = document.createElement("p");
temp.className = "temp";
temp.textContent = now.temperature_2m + " " + units.temperature_2m;
const wind = document.createElement("p");
wind.textContent = "Wind: " + now.wind_speed_10m + " " + units.wind_speed_10m;
card.append(heading, temp, wind);
} catch (error) {
card.innerHTML = "";
const message = document.createElement("p");
message.className = "error";
message.textContent = "Could not load the weather (" + error.message + "). Are you online?";
card.append(message);
} finally {
button.disabled = false;
}
}
select.addEventListener("change", showWeather);
button.addEventListener("click", showWeather);
showWeather();
</script>
</body>
</html>
Try these changes:
- Add your own city to the list. Search the web for its latitude and longitude.
- Add
relative_humidity_2mto thecurrentparameter (separated by a comma) and show the humidity on the card. - Turn off your Wi-Fi or mobile data, press Refresh and check that the error message appears.
Common Mistakes
- Forgetting to
await response.json().response.json()returns a promise. Withoutawait, you get a pending promise instead of your data. - Thinking
fetchfails on 404. It does not. Always checkresponse.okand throw your own error. - Sending an object without
JSON.stringify. Passingbody: newPostsends the text[object Object]. Convert it to a JSON string and set theContent-Typeheader. - No loading or error state. On a slow connection, an empty page looks broken. Show “Loading...” first and a friendly message in
catch. - Putting server data into
innerHTML. UsetextContentfor text that comes from an API, to keep the page safe.
Practice Task: Load Comments for a Post
The practice API has comments at https://jsonplaceholder.typicode.com/comments, and you can filter them with ?postId=1. Build a small page where the user types a post number and loads its comments. Your page should:
- Show “Loading...” while waiting.
- Build the URL with
URLSearchParams. - Check
response.okand throw an error when it is false. - List each comment’s
nameand show how many comments were found. - Show a friendly message if anything fails.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Post Comments</title>
</head>
<body>
<h2>Comments</h2>
<input id="postId" type="number" min="1" max="100" value="1">
<button id="load">Load comments</button>
<p id="status"></p>
<ul id="comments"></ul>
<script>
// Needs an internet connection
// TODO 1: when the button is clicked, show "Loading..." in #status
// TODO 2: fetch https://jsonplaceholder.typicode.com/comments with
// URLSearchParams so the URL ends with ?postId=<the number typed>
// TODO 3: if response.ok is false, throw an Error with the status
// TODO 4: show each comment's name in an <li>, and "5 comments" in #status
// TODO 5: on any error, show a friendly message in #status
</script>
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Post Comments</title>
</head>
<body>
<h2>Comments</h2>
<input id="postId" type="number" min="1" max="100" value="1">
<button id="load">Load comments</button>
<p id="status"></p>
<ul id="comments"></ul>
<script>
// Needs an internet connection
const statusText = document.querySelector("#status");
const list = document.querySelector("#comments");
document.querySelector("#load").addEventListener("click", async () => {
const postId = document.querySelector("#postId").value;
statusText.textContent = "Loading...";
list.innerHTML = "";
try {
const params = new URLSearchParams({ postId: postId });
const response = await fetch("https://jsonplaceholder.typicode.com/comments?" + params);
if (!response.ok) {
throw new Error("status " + response.status);
}
const comments = await response.json();
comments.forEach((comment) => {
const li = document.createElement("li");
li.textContent = comment.name;
list.append(li);
});
statusText.textContent = comments.length + " comments";
} catch (error) {
statusText.textContent = "Sorry, the comments could not be loaded (" + error.message + ").";
}
});
</script>
</body>
</html>
Key Takeaways
fetch(url)returns a promise of a Response. Useawait response.json()to get the data.fetchrejects only on network failure. Checkresponse.okand the status code yourself.- Build query strings with
URLSearchParamsso values are encoded correctly. - To send data, use
method: "POST", aContent-Typeheader andJSON.stringifyfor the body. - Always show loading and error states, and use
AbortControllerto stop requests that take too long. - CORS is the server’s permission for other websites to read its data. Without it, the browser blocks the response.