Just as the name suggests, the Fetch API is an easy way to fetch resources from a remote or local server through a JavaScript interface.
This means that the browser can directly make HTTP requests to web servers. It’s made possible by the fetch()
method, which returns a promise, whose response is fulfilled using the Response
object.
The fetch()
method takes a mandatory argument, which is the path to the remote server or source from which we want to fetch data. Once the response is received, it then becomes the developer’s responsibility to decide how to handle the body content and display it in some HTML template, for example.
Below is a somewhat basic fetch()
example that grabs data from a remote server:
fetch(url) .then( //handle the response ) .catch( //handle the errors )
The example above uses simple promises to implement the fetch()
method. We specify a URL, and store it in a const
variable. In our case, the remote server URL is random, and only for exemplary purposes.
Over the course of this article, we’ll be fine-tuning this code snippet to show how to make maximum use of the Fetch API when making calls.
fetch()
methodTo follow through with this guide, you’ll need an understanding of basic JavaScript concepts such as promises, a``sync/``a``wait
, and callback functions
For this tutorial, we want to simulate an environment where you would be working with an API. To do that, we’ll use a JSON placeholder, a free and fake API for testing, and it will serve as the means through which we reach our server.
GET
requestsA GET
request is used to retrieve data from a server. By default, all HTTP requests are GET
unless specified otherwise.
For example, if we’re building a to-do list app, we need to fetch and display tasks on the front end. Using JavaScript, we can target an unordered list (ul
) in our HTML and populate it dynamically with the fetched data. Here’s how we can set up the HTML structure:
<ul id="list"> </ul>
When we enter a to-do list item, it gets stored on our server. To retrieve those items, we need to use the GET
request.
The first thing we’ll do in our JavaScript is get the ul
element from the DOM via its ID so that we can append list items to it later:
const ul = document.getElementById("list")
We then go on to store the URL of the API that connects us to the remote server in a variable called URL
:
const url = "https://jsonplaceholder.typicode.com/todos"
We’ve gotten the variables we need. Now we can get to working with fetch()
. Remember that the fetch()
method takes into account just one parameter: the URL. In this case, we’ll pass in our url
variable:
fetch(url)
This alone won’t give us the data we need since the response isn’t in JSON
format. We need to parse it so that we can work with the data and display it in our HTML. To do this, we use the .json()
method:
fetch(url) .then(response => response.json())
After fetch()
is run, it returns a promise that’s resolved to a Response
object. The .then()
method above is used to process this Response
object. The .json()
method is called on the object, and it returns another promise that resolves to the JSON data we need:
fetch(url) .then(response => response.json()) .then(data => { })
The second .then()
above is used to handle the JSON data returned by the previous .then()
. The data
parameter is the parsed JSON data.
Now that we have the data, how do we output it to the HTML template to make it visible on the page?
We have a ul
element, and we want to display the data as a list of to-do items. For each to-do item that we fetch, we’ll create a li
element, set the text to the item we’ve fetched, and then append the li
to the ul
element.
Here’s how to achieve this result:
fetch(url) .then(response => response.json()) .then(data => { data.forEach(todo => { const li = document.createElement("li") li.innerText = todo.title ul.appendChild(li) }) })
Our complete JavaScript logic should look like so:
const ul = document.getElementById("list") const url = "https://jsonplaceholder.typicode.com/todos" fetch(url) .then(response => response.json()) .then(data => { data.forEach(todo => { const li = document.createElement("li") li.innerText = todo.title ul.appendChild(li) }) })
With this, we should see a list of to-do items displayed on the webpage. By default, JSONPlaceholder returns a maximum of 200 to-do items, but we can always play around with our logic to decrease this number. We can get more than just the title of our to-do items. An example of more data we could find might be the status of the task.
POST
requestsNow that we’ve seen how to get data from the server, we’ll see how to add data to the server. Imagine filling out a form or entering data on a website. This data needs to get to the database somehow, and we usually achieve this using POST
requests.
In our HTML code, we’ll have a small form with an input field and a submit button:
<form id="todo-form"> <input type="text" id="todo-input" placeholder="Enter your task here..." required> <button type="submit">Add To-do</button> </form>
Then over in JavaScript, we want to grab two elements and specify the URL, through which we make requests:
const form = document.getElementById("todo-form"); const input = document.getElementById("todo-input"); const url = "https://jsonplaceholder.typicode.com/todos"
We’ll create an event listener so that the request is made each time we submit the form:
form.addEventListener("submit", (event) => { event.preventDefault() })
Data is usually sent to the backend as objects with specific key-value pairs representing the format in which we want to store our data. In our case, we want to store the title and status of a to-do item.
For example:
const newToDo = { title: input.value, completed: false, }
Now, we can start setting up our POST
request. Unlike the GET
request, which only requires one parameter, the POST
request needs two. The first is the URL, and the second is an object that includes the method
, body
, and headers
keys:
fetch(url, { method: "POST", body: JSON.stringify(newTodo), headers: { "Content-Type": "application/json", }, })
The method
key defines the type of request being made. In this case, it’s set to POST
, indicating that we’re sending data to the server. The body
contains the data, formatted as a JSON string using JSON.stringify(newTodo)
. We’ll cover the headers
in the next section.
These are the basics of a simple [POST request](https://blog.logrocket.com/how-to-make-http-post-request-with-json-body-in-go/)
. Our final JavaScript logic will look something like this:
const form = document.getElementById("todo-form"); const input = document.getElementById("todo-input"); const url = "https://jsonplaceholder.typicode.com/todos"; form.addEventListener("submit", (event) => { event.preventDefault(); const newTodo = { title: input.value, completed: false, }; fetch(url, { method: "POST", body: JSON.stringify(newTodo), headers: { "Content-Type": "application/json", }, }); });
Besides GET
and POST
, there are a variety of other operations that you can use when working with data. You can visit the MDN docs to learn more about these requests and how to use them.
So far, our GET
and POST
examples assume everything goes smoothly—but what if they don’t? What happens if the resource doesn’t exist or a network error occurs while sending data?
To handle these cases, we can append a .catch
method to catch network errors and check the response status to handle HTTP errors. Let’s look at how to make our fetch requests more resilient.
Let’s revamp the code above to account for potential errors:
form.addEventListener("submit", (event) => { event.preventDefault(); const newTodo = { title: input.value, completed: false, }; fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(newTodo), }) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } console.log("Todo added") }) .catch((error) => { console.error("Error:", error); }); });
We use .then
to check if the response is OK—if not, we throw an error with the status code. Then, .catch
handles any fetch errors and logs them to the console.
Proper error handling ensures issues are caught and communicated effectively. HTTP status codes play a key role here, as each one has a specific meaning and requires a different response. Some of the most common status codes include:
200 OK
404 Not Found
500 Internal Server Error
When working with large amounts of data, we can’t always wait to completely fetch all the data from the server before we process it. Streaming is a technique that allows us to process data in chunks. The data is processed this way until it’s all retrieved from the server. This is a way of improving application responsiveness and performance.
Let’s transform our previous GET
request example into one that implements streaming.
We initiate the request to the API and check if the response is OK:
fetch(url) .then(response => { if(!response.ok){ throw new Error(`HTTP error! status: ${response.status}`) } })
We need a reader to read the response body in chunks and decode the chunks into text. This decoding is done with the help of TextDecoder()
:
const reader = response.body.getReader() const decoder = new TextDecoder() let result = ""
After reading the first chunk, we process it. If the stream is complete, it logs a message to the console and returns. If not, it decodes the chunk of data, appends it to the result string, parses it as JSON, and appends each to-do item to the list.
It then reads the next chunk of data and processes it recursively:
return reader.read().then(function processText({ done, value }) { if (done) { console.log("Stream complete") return } //decode and parse JSON result += decoder.decode(value, { stream: true }); const todos = JSON.parse(result) //add each to-do to the list todos.forEach((todo) => { const li = document.createElement("li") li.innerText = todo.title list.appendChild(li) }) return reader.read().then(processText) })
At the end, we can add a .catch
to account for any errors:
.catch((error) => { console.error("Error", error) })
Our final JavaScript file for streaming would look like this:
const list = document.getElementById("to-do"); const url = "https://jsonplaceholder.typicode.com/todos"; fetch(url) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader() const decoder = new TextDecoder() let result = "" return reader.read().then(function processText({ done, value }) { if (done) { console.log("Stream complete") return } result += decoder.decode(value, { stream: true }); const todos = JSON.parse(result) todos.forEach((todo) => { const li = document.createElement("li"); li.innerText = todo.title list.appendChild(li) }) return reader.read().then(processText); }) }) .catch((error) => { console.error("Error:", error); })
In addition to serving and receiving data from the client, the server needs to understand every request it receives. This is possible with the help of headers, which act as metadata and accompany requests. These key-value pairs tell the server what kind of request the client is making, and how to respond to it.
To understand headers, we must discuss the two categories that exist: fetch request headers and fetch response headers.
As the name implies, request headers tell the server what kind of request you’re making and may include conditions the server needs to fulfill before responding.
In our POST
example, we used the Content-Type
header to specify that we were sending JSON data. Another important header is Authorization
, which carries authentication details like tokens or API keys for secure API access. The Accept
header tells the server which data format we prefer in the response.
Some headers, known as forbidden header names, are automatically set by the browser and can’t be modified programmatically. These are called forbidden header names.
When the server processes a request, it responds with headers that provide important details about the response.
Key response headers include Status Code
, which indicates whether the request was successful (200 OK
) or encountered an issue (500 Server Error
). Content-Length
specifies the size of the returned data, while Content-Type
reveals the format, such as JSON or HTML.
Some headers function in both requests and responses, like:
Cache-Control
: Manages browser caching behavior.Accept-Encoding
: Tells the server which compression formats the client supports.Headers help streamline communication between the client and server, improving response handling. For a deeper dive, check out this full list of headers.
The Fetch API is the modern standard for making requests in JavaScript, but it’s useful to compare it with older methods like Axios and XMLHttpRequest
to understand its advantages. Here’s a comprehensive comparison:
Feature | Fetch API | Axios | XMLHttpRequest |
---|---|---|---|
JSON handling | Manual parsing is needed | Automatic JSON parsing | Manual parsing needed |
Error handling | Requires manual checks (e.g., adding .catch() methods and !response.ok ) |
Built-in error handling | Complex and inconsistent |
Browser support | Mordern browsers only | Mordern browsers | Excellent browser support including old browsers |
Cancellation of requests | Using AbortController , this is supported |
Supported | Does not support request cancellation |
Syntax | Makes use of promises, with a short and clean syntax | Also makes use of promises | Uses mostly callbacks and is too verbose |
In this guide, we’ve covered the fundamentals of the Fetch API, from making simple GET
and POST
requests to handling errors, working with headers, and managing streaming responses. While there’s much more to explore, this foundation equips you to confidently use fetch
in JavaScript. Mastering these concepts will help you build more efficient and reliable web applications.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how TypeScript enums work, the difference between numeric and string enums, and when to use enums vs. other alternatives.
Review the basics of react-scripts, its functionality, status in the React ecosystem, and alternatives for modern React development.
Explore the fundamental commands for deleting local and remote branches in Git, and discover more advanced branch management techniques.
AbortController
APICheck out a complete guide on how to use the AbortController and AbortSignal APIs in both your backend and frontend.