Why JSON APIs matter
If you write JavaScript for any length of time, you will fetch data from an API. Weather apps, social feeds, payment systems, maps — they all return data as JSON. Understanding how to fetch, read, and handle this data correctly is one of the most fundamental skills in frontend and Node.js development. This guide walks through everything you need, with patterns I personally use in real projects.
The basics: fetch() and .json()
The modern approach uses the Fetch API, which is built into every browser and Node.js 18+. Here is the simplest possible example:
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);Notice the two awaits. fetch() returns a Promise that resolves to a Response object — not the data itself. The second .json() call reads the response body and parses it as JSON. Both are async operations, and forgetting either await is the #1 mistake beginners make.
Always check the response status
Here is something that catches a lot of developers off guard: fetch() does not throw an error when the server returns a 404 or 500. It only rejects if there is a network failure — no connection, DNS resolution failure, that sort of thing. An HTTP error response is still a "successful" fetch as far as the Promise is concerned.
This means you must check response.ok, which is true for any status code in the 200–299 range:
const response = await fetch('https://api.example.com/users/42');
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const user = await response.json();I have debugged countless issues where someone was confused about why their code "worked" but showed no data — the API was returning a 401 or 403, and the code silently ignored it.
Wrapping in async functions with try/catch
Real production code needs error handling. Network requests can fail for many reasons: the user goes offline, the API is down, CORS blocks the request, the server returns malformed JSON. A try/catch block handles all of these:
async function getUser(id) {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
console.error('Failed to fetch user:', err.message);
return null; // or throw, depending on your app
}
}I prefer returning null on error for UI code (so the UI can show an empty state), and re-throwing for utility functions (so the caller can decide how to handle it).
Sending JSON to an API: POST requests
Reading from an API is only half the story. Sending data requires a POST (or PUT/PATCH) request with proper headers:
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello', published: true })
});The Content-Type header tells the server how to interpret the body. Forgetting it often results in the server treating your data as plain text, which leads to confusing errors. Always use JSON.stringify() on the body — you cannot pass a JavaScript object directly.
Working with nested data
Real API responses are rarely flat. You often receive deeply nested objects. Access them with standard dot notation or destructuring:
const { user: { name, email }, posts } = await response.json();
// Or safely with optional chaining:
const city = data?.user?.address?.city ?? 'Unknown';Optional chaining (?.) is invaluable when you are not certain a field will be present — which is almost always. APIs change, fields get added and removed, and defensive access prevents your app from crashing on unexpected shapes.
Pagination: handling large datasets
Many APIs return results in pages. A common pattern is to loop until there are no more pages:
let page = 1;
let allResults = [];
while (true) {
const data = await fetchPage(page);
allResults = allResults.concat(data.results);
if (!data.next_page) break;
page++;
}Be careful with this pattern — always have a max iteration count to avoid infinite loops if the API has a bug in its pagination metadata.
Common mistakes to avoid
Forgetting both awaits. fetch() and .json() are both async. Missing either one gives you a Promise object instead of data. Not checking response.ok. Error responses are not exceptions — you have to check manually. CORS issues with third-party APIs. If you are calling an API from a browser, the server must send CORS headers. This cannot be worked around client-side. Hardcoding API keys. Never put secret keys in client-side JavaScript — they are visible to anyone who views source. Use a backend proxy or environment variables in a build step.
Debugging: paste into my JSON formatter
When an API returns something unexpected, copy the raw response body and paste it into my JSON formatter. It shows you the exact structure with syntax highlighting, collapsible nodes, and instant validation. I use it constantly when integrating with a new API — it is much faster than console.log-ing everything or squinting at minified output in the Network tab.