Synchronous vs Asynchronous Code
AI Using GitHub Copilot in VS Code
In earlier chapters you used tools like curl and Postman to make HTTP requests to external web APIs. In this chapter we will do the same thing directly from our JavaScript code using the Fetch API in Node.js.
We will:
fetch() to make HTTP requests in Node.js.POST requests with JSON bodies.<aside> 💭
The examples in this chapter are meant to run in Node.js, not in the browser. Modern versions of Node (v18 and later) provide a built‑in fetch function globally, so you do not need to install anything for the basic examples.
</aside>
fetch?You have already seen a few different ways to talk to web APIs:
curl in the terminal.Both do essentially the same thing: they send HTTP requests and show you the HTTP responses.
fetch is a JavaScript API that lets you do the same thing from your code. It:
In the browser, fetch has been available for a long time. In Node.js it is now also available globally in recent versions, so you can simply write:
const url = '<https://jsonplaceholder.typicode.com/posts/1>';
fetch(url);
The important part is that fetch(url) returns a promise, which you should handle using the techniques you learned in the Promises chapter.
Let us start with a simple GET request that fetches a single post and logs it.
const url = '<https://jsonplaceholder.typicode.com/posts/1>';
fetch(url)
.then((response) => {
console.log('Status:', response.status);
console.log('OK?:', response.ok);
return response.json(); // parse body as JSON
})
.then((data) => {
console.log('Data:', data);
})
.catch((error) => {
console.error('Network or parsing error:', error);
});
/*
Possible output:
Status: 200
OK?: true
Data: {
userId: 1,
id: 1,
title: 'sunt aut facere repellat ...',
body: 'quia et suscipit ...'
}
*/
Let’s connect this to what you already know:
fetch(url) returns a pending promise.Response object.response.json(), which also returns a promise..then() receives the parsed JSON data..catch() handles any error that happened anywhere in the chain (network issues, invalid JSON, etc.).From a Promises perspective, fetch is “just” another function that returns a promise.
There is an important detail about fetch that surprises many developers the first time they see it:
<aside> ⚠️
fetch only rejects its promise on network errors, not on HTTP errors like 404 or 500.
</aside>
In other words:
.catch() handler runs.Response object with response.ok === false.This means you must check the response.ok or response.status manually and treat a bad status as an error yourself.
const url = '<https://jsonplaceholder.typicode.com/this-path-does-not-exist>';
fetch(url)
.then((response) => {
console.log('Status:', response.status); // e.g. 404
if (!response.ok) {
// Turn this into a rejected promise so it is handled by .catch()
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log('This will not run for a 404');
console.log('Data:', data);
})
.catch((error) => {
console.error('Error while fetching:', error.message);
});
/*
Possible output:
Status: 404
Error while fetching: Request failed with status 404
*/
You can compare this to how you handled errors in the Promises and File I/O chapters:
fsPromises.appendFile.throw‑ing inside the .then() handler.You often need to send query parameters like ?userId=1&page=2. You could manually concatenate strings, but Node.js provides a safer option: the URL class.
const BASE_URL = '<https://jsonplaceholder.typicode.com/posts>';
const url = new URL(BASE_URL);
url.searchParams.set('userId', '1'); // adds ?userId=1
console.log('Full URL:', url.toString());
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((posts) => {
console.log(`Received ${posts.length} posts`);
console.log(posts.map((post) => post.title));
})
.catch((error) => {
console.error('Error:', error.message);
});
/*
Possible output:
Full URL: <https://jsonplaceholder.typicode.com/posts?userId=1>
Received 10 posts
[
(ommitted for brevity)
]
This is very similar to how you might add query parameters in Postman or curl, but now you are doing it inside your JavaScript code.
Most real‑world APIs require custom headers:
Content-Type to indicate the body format.Accept to tell the server what format you expect back.Authorization or X-API-Key.You pass headers as part of the second argument to fetch:
const url = '<https://api.example.com/protected-resource>';
fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
'X-API-Key': 'your-api-key-here', // example value
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log('Protected data:', data);
})
.catch((error) => {
console.error('Error:', error.message);
});
<aside> ❗
Never commit real API keys or other secrets to Git. Use environment variables or configuration files that are not part of your version control history.
</aside>
For this module we will mostly work with public APIs that do not require authentication, but the pattern remains the same.
Up to now we have only made GET requests. Let us now send a POST request with a JSON body.
In Postman you would:
POST.Content-Type: application/json.In fetch you do the same, but in JavaScript:
const url = '<https://jsonplaceholder.typicode.com/posts>';
const newPost = {
title: 'Hello from Node fetch',
body: 'This is the body of the post.',
userId: 1,
};
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
})
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to create post: ${response.status}`);
}
return response.json();
})
.then((createdPost) => {
console.log('Created post:', createdPost);
})
.catch((error) => {
console.error('Error:', error.message);
});
/*
Possible output:
Created post: {
title: 'Hello from Node fetch',
body: 'This is the body of the post.',
userId: 1,
id: 101
}
*/
This pattern (method + headers + JSON body) is extremely common when working with HTTP APIs.
fetch to curl and PostmanLet us look at the same request in three different tools.
curl
curl -X POST \\
-H "Content-Type: application/json" \\
-d '{"title":"Hello","body":"World","userId":1}' \\
<https://jsonplaceholder.typicode.com/posts>
Postman
POSThttps://jsonplaceholder.typicode.com/postsContent-Type: application/json{ "title": "Hello", "body": "World", "userId": 1 }Node.js with fetch
fetch('<https://jsonplaceholder.typicode.com/posts>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Hello',
body: 'World',
userId: 1,
}),
})
.then((response) => response.json())
.then((data) => {
console.log('Response:', data);
})
.catch((error) => {
console.error('Error:', error.message);
});
The HTTP information is the same in all three cases. Only the interface and syntax change.
You have already seen some typical promise mistakes in the Promises chapter. When using fetch, the same rules apply.
response.json()If you forget the return in a .then() block, the next .then() receives undefined.
// ❌ Wrong: missing return
fetch('<https://jsonplaceholder.typicode.com/posts/1>')
.then((response) => {
response.json(); // this promise is ignored
})
.then((data) => {
console.log('Data:', data); // Data: undefined
})
.catch((error) => {
console.error('Error:', error);
});
Correct:
// ✅ Correct: return the promise from response.json()
fetch('<https://jsonplaceholder.typicode.com/posts/1>')
.then((response) => {
return response.json();
})
.then((data) => {
console.log('Data:', data);
})
.catch((error) => {
console.error('Error:', error);
});
Or using an implicit return:
fetch('<https://jsonplaceholder.typicode.com/posts/1>')
.then((response) => response.json())
.then((data) => {
console.log('Data:', data);
})
.catch((error) => {
console.error('Error:', error);
});
response.okIf you never check response.ok, your code might happily call response.json() even for a 404 or 500 response.
// ❌ Not checking response.ok
fetch('<https://jsonplaceholder.typicode.com/this-path-does-not-exist>')
.then((response) => response.json())
.then((data) => {
console.log('Maybe error page, maybe not:', data);
})
.catch((error) => {
console.error('Network error only:', error);
});
Better:
// ✅ Check response.ok
fetch('<https://jsonplaceholder.typicode.com/this-path-does-not-exist>')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log('This will only run for 2xx responses:', data);
})
.catch((error) => {
console.error('Request failed:', error.message);
});
This is completely in line with the way you used promises in the asynchronous file I/O chapter: treat errors explicitly and let them be handled in one central .catch().
In this chapter you learned that:
fetch(url) in Node.js returns a promise that resolves to a Response object.response.json() (which also returns a promise) to read the body as JSON.fetch only rejects on network errors; HTTP errors must be detected by checking response.ok or response.status.fetch(url, options).POST requests with JSON bodies use Content-Type: application/json and JSON.stringify.curl and Postman now appear inside your JavaScript code.