JavaScript course menu
Fetching Data from APIs with JavaScript fetch()
Learn to call APIs with the JavaScript fetch API: GET requests, reading JSON with response.json(), checking response.ok and status codes, error handling, POST requests with JSON bodies and headers, query strings, loading states, AbortController timeouts, and CORS.
On this page
An API (application programming interface) lets your code ask a server for data or tell it to do something. On the web, most APIs are called over HTTP and exchange JSON. The built-in fetch function makes these requests, in browsers and in Node.js (version 18 and later).
The examples use JSONPlaceholder (jsonplaceholder.typicode.com), a free public test API that returns the same fake data every time, so the outputs below are real.
A GET request#
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await response.json();
console.log(response.status);
console.log(user.name);
console.log(user.address.city);200
Leanne Graham
GwenboroughTwo steps, both asynchronous:
await fetch(url)resolves to a Response once the server's headers arrive.await response.json()reads the body and parses it as JSON. (response.text()gives the raw text instead.)
Checking for HTTP errors#
fetch only rejects on network failures (no connection, DNS errors, blocked requests). An HTTP error such as 404 Not Found or 500 Server Error still resolves, so you must check response.ok yourself:
const response = await fetch("https://jsonplaceholder.typicode.com/users/9999");
console.log(response.ok, response.status);false 404A reusable helper that handles both kinds of failure:
async function getJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
try {
const posts = await getJson("https://jsonplaceholder.typicode.com/posts?userId=1");
console.log(`${posts.length} posts, first: "${posts[0].title.slice(0, 30)}..."`);
await getJson("https://jsonplaceholder.typicode.com/nope");
} catch (error) {
console.log(error.message);
}10 posts, first: "sunt aut facere repellat provi..."
Request failed: 404 Not FoundQuery strings#
Build query strings with URLSearchParams, which encodes special characters for you:
const params = new URLSearchParams({ userId: 2, _limit: 3 });
const url = `https://jsonplaceholder.typicode.com/todos?${params}`;
console.log(url);
const todos = await (await fetch(url)).json();
console.log(todos.map((t) => t.id));https://jsonplaceholder.typicode.com/todos?userId=2&_limit=3
[ 21, 22, 23 ]Sending data: POST#
To send data, pass an options object with the method, headers and a body:
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "My first post", body: "Hello!", userId: 1 }),
});
const created = await response.json();
console.log(response.status, created);201 { title: 'My first post', body: 'Hello!', userId: 1, id: 101 }method:"POST","PUT","PATCH"or"DELETE"(default"GET").headers:Content-Type: application/jsontells the server the body is JSON. Authenticated APIs usually also need anAuthorizationheader.body: a string, so objects go throughJSON.stringify. Form data can be sent asnew FormData(form)instead.
JSONPlaceholder pretends to save the post (and always gives it id 101) but does not really store it.
Showing data in a page#
In a web page, show a loading state, render the data, and handle errors:
<button id="load">Load users</button>
<p id="status"></p>
<ul id="users"></ul>const status = document.querySelector("#status");
const list = document.querySelector("#users");
document.querySelector("#load").addEventListener("click", async () => {
status.textContent = "Loading...";
list.replaceChildren();
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users?_limit=5");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const users = await response.json();
list.replaceChildren(...users.map((u) => {
const li = document.createElement("li");
li.textContent = `${u.name} (${u.email})`;
return li;
}));
status.textContent = `Loaded ${users.length} users`;
} catch (error) {
status.textContent = `Could not load users: ${error.message}`;
}
});The example uses textContent for API data, never innerHTML, so that data containing HTML cannot inject scripts.
Timeouts with AbortController#
fetch has no built-in timeout, but it can be cancelled with an abort signal. AbortSignal.timeout(ms) creates one that fires automatically:
try {
await fetch("https://jsonplaceholder.typicode.com/photos", {
signal: AbortSignal.timeout(1),
});
} catch (error) {
console.log(error.name);
}TimeoutErrorWith a manual AbortController, you can also cancel a request yourself, for example when the user types a new search before the previous one finishes.
CORS#
A page can only read responses from another origin (domain) if that server allows it with CORS (Cross-Origin Resource Sharing) headers. If not, the browser blocks the response and shows a CORS error in the console, even though the request reached the server. CORS is set on the server; you cannot fix it from front-end code. Node.js does not apply CORS, which is why scripts on a server can call APIs that a browser page cannot.
Keep secrets on the server#
Anything in front-end JavaScript can be read by visitors, including API keys. Keys for paid or private APIs must be used from your server (for example, a Node.js API route), which then calls the external API on the browser's behalf.
Practice#
Practice exercise
Fetch the todos for userId=1 from JSONPlaceholder, then print how many there are, how many are completed, and the titles of the first two that are not completed.
Show solution
const response = await fetch("https://jsonplaceholder.typicode.com/todos?userId=1");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const todos = await response.json();
const done = todos.filter((t) => t.completed);
const pending = todos.filter((t) => !t.completed);
console.log(todos.length, "todos");
console.log(done.length, "completed");
console.log(pending.slice(0, 2).map((t) => t.title));20 todos
11 completed
[ 'delectus aut autem', 'quis ut nam facilis et officia qui' ]Key takeaway#
Finished this lesson?