Making GET, POST, PUT, and DELETE requests with Fetch - v2

Anton Ioffe - October 13th 2023 - 18 minutes read

With an ever-growing significance of JavaScript in modern web development, APIs have become the inescapable touchstones that lead to streamlining data fetch and manipulation, user-experience enhancement, and building agile applications. Among these, the Fetch API provides a compelling interface for fetching resources. This article is a comprehensive exploration into leveraging the Fetch API for handling HTTP requests - GET, POST, PUT, and DELETE with JavaScript.

By taking you step-by-step through each of these HTTP methods, the article aims to develop a detailed understanding of these methods' subtilites, intricacies, and techniques to operate in-sync with the Fetch API. Following practical examples, we will understand the typical challenges and potential pitfalls to avoid, and we'll discuss error handling and performance issues, enabling you to unlock powerful features in your web development toolkit.

More than merely a tutorial, this article opens vistas for thought-provoking discussions on best-practices and advanced implementation approaches. Whether you want to bolster your Fetch fundamentals or take it a notch higher by integrating advanced concepts like headers manipulation, managing parameters, and async/await, this article has it covered. Prepare to dive deep into the world of JavaScript Fetch, needless to mention, some captivating sections that warrant your undivided attention await you.

Fetch API Overview and Key Terms

Fetch API, an integral part of JavaScript, is a promise-based interface that fetches resources by making HTTP requests to servers from browsers. The term 'promise-based' implies that fetch() returns a Promise which is fulfilled once the response is available. The syntax for making a fetch request looks like this: fetch(resourceUrl [, options]).

Performing a simple fetch request looks like:

fetch('/example.com/data')
    .then(response => response.json())
    .then(data => console.log(data));

In this case, the fetch() method directs a request to the specified resource URL ('/example.com/data'). The response.json() method parses the Response object into JSON, and finally, the parsed JSON data is logged to the console.

The Fetch API allows JavaScript applications to make asynchronous network requests. In other words, the browser doesn't have to block and wait for the fetch request to finish before carrying on with the execution of the next lines of code. This is critical in building responsive applications where you don't want user interaction to be interrupted by network requests.

The Response object encapsulates the results of the fetch request. It represents the entire HTTP response, however, it does not directly contain the response body. To access the body text, specific methods must be used like response.arrayBuffer(), response.blob(), response.error(), response.formData(), response.json(), or response.text(). Each of these methods returns a Promise that resolves with a different format of the response body.

An important caveat, however, is that you can read the response only once. After the first reading, the response body is empty. To read the response again, you have to clone it first, i.e., let clone = response.clone().

Key terms to understand when working with the Fetch API include:

  • Resource: This refers to one unique data entry which can be fetched over a network.
  • URI (Uniform Resource Identifier): This is a sequence of characters that identifies a logical or physical resource. In the context of Fetch, it is often the URL to which HTTP requests are made. For instance, in fetch('https://example.com/data'), 'https://example.com/data' is the URI.
  • Promise: This is an object that may produce a single value some time in the future. Promises are used to handle asynchronous operations, allowing you to have asynchronous code that is easier to write and read.
  • Promise Chaining: If a callback function returns a value, this value becomes the fulfillment value of the promise returned by then(). If it throws an error, that becomes the rejection reason. If it returns a Promise, the fulfillment of the returned Promise is used instead.

A final point to note is that Fetch API offers significant improvements over older methods of making HTTP requests, such as XMLHttpRequest (XHR). It brings both power and simplicity to the process of making network requests in JavaScript. Furthermore, being promise-based, it's easily incorporable into modern JavaScript applications where async/await syntax is commonly used.

Fetching Data with GET Requests

Understanding GET Requests

In the realm of HTTP methods, GET serves as a read-only method that's often utilized to fetch data from an API endpoint. As a read-only method, GET requests pose no risk of mutating or corrupting data since they simply aren't designed for that.

Let's dive into a practical example. Suppose we want to fetch an array of to-do tasks from an API. For this scenario, we're turning to the fake API https://jsonplaceholder.typicode.com/todos, an endpoint teeming with 200 unique data entries of to-dos.

Making a GET Request with Fetch API

In comes Fetch API, stepping up as a superior, modern alternative to the traditional XMLHttpRequest, making it our preferred candidate for issuing our network requests. Among the toolset of the Fetch API is fetch(), a simple yet efficient method tailor-made for HTTP requests.

To demonstrate, here's how you would structure a fetch-driven GET request:

// Initiate a GET request to the target API endpoint
fetch('https://jsonplaceholder.typicode.com/todos')
    // Fetch API returns a Promise that resolves to a Response object. We deal with this object by parsing its body text as JSON. 
    .then(response => response.json())
    // Handle the data returned from the GET request and log it to the console
    .then(data => console.log(data));

A line-by-line walkthrough of this snippet reveals the following:

  1. The fetch() method kicks off the HTTP GET request, with no need for any extra details or parameters because we're dealing with a GET request.

  2. On the heels of the fetch() call is a Promise that eventually gets fulfilled with a Response object, which represents the request's response.

  3. Once that promise is fulfilled, the .then() method comes in handy to access and deal with this Response. Here, the Response.json() method parses the response body text as JSON.

  4. Afterwards, we introduce another .then() method to manage the actual data, in its JSON format, returned from the GET request. In our example, we're simply logging the data to the console. However, real-world applications might dictate displaying this data within a user interface or other equivalent scenarios.

As demonstrated here, JavaScript promises used for managing asynchronous operations entail chaining .then() methods, an approach that works well for simpler use cases. However, this method can give rise to complex and confusing code structures, especially with multiple nested .then() calls.

Understanding the Caveats

Always bear in mind that server data fetching is inherently asynchronous, making error handling a vital part of the process. Our initial example did not consider adding any error handling capabilities via the .catch method, which would make debugging any potential network errors unnecessarily challenging.

Here's how you can insert error handling capabilities into a fetch request:

// Initiate a GET request to the target API endpoint
fetch('https://jsonplaceholder.typicode.com/todos')
    // Fetch API returns a Promise that resolves to a Response object. We deal with this object by parsing its body text as JSON.
    .then(response => response.json())
    // Handle the data returned from the GET request and log it to the console
    .then(data => console.log(data))
    // Handle any potential errors during either the request execution or within our .then() handlers
    .catch(error => console.error('Error:', error));

The added .catch method aids in managing any potential errors unveiled during the execution of the request or within our .then() handlers.

Parting Thoughts

The GET method offers developers a powerful tool to retrieve data from an API endpoint and leverage it within their applications. However, understanding how to effectively make these requests and handle any potential issues are pivotal to leveraging the GET method appropriately. While using the Fetch API for these requests, consider these queries to ensure efficient web app development:

  1. How will you manage the memory utilization and performance of your app when dealing with large data fetch operations?

  2. How is your fetch-driven code maintaining readability and how can it be effectively explained to other developers, particularly when handling varied and nested response data?

  3. Are you implementing effective error handling to tackle failed or partial network requests, especially GET requests?

  4. What impact does data fetching have on the user experience of your web app, and how can you mitigate long load times or enhance data rendering?

Creating Resources with POST Requests

Creating Resources with POST Requests

The POST method in the HTTP protocol is designated for creating new resources on a server. This method sends data to the server, and as a result, a new resource is created subordinate to some other parent resource. When a new resource is POSTed to the parent, the API service generally assigns it a unique ID. Let's deep dive into making POST requests with the JavaScript Fetch API.

Anatomy of a POST Request with Fetch

A POST request using the Fetch API needs to specify the method as 'POST', include data to be sent in the body property, and if necessary, define the correct content-type header.

Here's a simple illustration of how a fetch call for a POST request looks like:

fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST', 
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'foo',
    body: 'bar',
    userId: 1
  })
})
.then(response => response.json()) 
.then(data => console.log(data))
.catch((error) => console.error('Error:', error));

In this example, we make a POST request to the 'https://jsonplaceholder.typicode.com/posts' endpoint. In the request, we specify the method as 'POST' and define a header to inform the server that we're sending JSON data. The body of our request contains the data we want to create on the server, converted to a JSON string using JSON.stringify.

The Fetch API includes promise-based architecture, so we can then chain .then() methods to handle the response.

Common Mistakes

One common mistake developers often make when working with POST requests in Fetch is not setting the correct Content-Type header. The server needs this header to correctly process the incoming request body. If you omit it, the server may not understand your request, leading to errors or unexpected behavior.

Here's an erroneous example:

// Don't do this
fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  body: JSON.stringify({
    title: 'foo',
    body: 'bar',
    userId: 1
  })
})...

In the above erroneous example, we've omitted the header for 'Content-Type'. The correct code should include a headers object like this:

// Corrected example
fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'foo',
    body: 'bar',
    userId: 1
  })
})...

Best Practices

When making POST requests using Fetch, it's often a good practice to centralize error handling within the fetch call. This way, you can consistently handle possible errors from the server and make your code cleaner and more maintainable.

Another practice is to always stringify your JSON body payload using JSON.stringify and set the Content-Type header to 'application/json' when sending JSON to a server.

Finally, queries that change or create data should be idempotent, meaning they should produce the same result even if repeated multiple times. If a client sends the same POST request multiple times, you should design your back end to check if it already has an entity with the same unique property before creating another one.

In conclusion, the POST method is a pillar of the HTTP protocol that allows developers to create new resources on servers. Adopting proper usage and maintaining best practices in crafting POST requests using Fetch is crucial to building efficient JavaScript applications.

Are you currently mindful of setting the right headers when making POST requests with Fetch? How idempotent are your server operations? These can be significant in making your web application more robust and efficient.

Updating Resources with PUT Requests

In the realm of JavaScript Fetch requests, the PUT method holds a critical role. It enables developers to update existing server resources, fundamentally stirring the dynamism of web applications. In this comprehensive exploration, we'll scrutinize how to utilize PUT requests with Fetch in JavaScript through real-life use cases while discussing prospective traps and efficiency concerns.

Firstly, it's crucial to keep in mind that the PUT method requires the specific ID of the resource intended for updating. So, before placing a PUT request, you must be aware of the exact resource you aim to modify.

Consider an example where we are updating a blog post on a fictive RESTful API. Here is how a PUT request might look via Fetch:

fetch('https://api.yourwebsite.com/posts/1',{
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        title: 'Updated Blog Title',
        body: 'New creative content for the blog post.'
    })
})
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

In the code above, we target the blog post with an ID of '1'. Using fetch and designating the method as 'PUT', we incorporate headers and indicate that we are sending a JSON-formatted body. We then stringify the new blog content—the title and body—and place this within the body of the PUT request. If the blog structure includes additional keys, you should specify those keys within the body too.

Despite its usefulness, there are potential pitfalls and points about efficiency to consider with PUT usage in JavaScript Fetch.

Pitfall: Overwriting Data

One common error with PUT requests is inadvertent data overwriting. A PUT request updates current resources with new data. Nonetheless, if some fields are not included in the new data or are left undefined, those fields might be erased. To avoid unintended data loss, always scrutinize the data you're putting to the server.

Efficiency: PUT vs. PATCH

The question often arises: "Considering efficiency, should I use PUT or PATCH to update a resource?" The crucial distinction is that while a PUT request replaces the entire resource with the provided data, a PATCH request only updates the parts of the resource that are included in the request. While both methods require similar resources to execute a single field update, PATCH can be more efficient when only a certain part of the resource requires modification. This focused approach of PATCH spares computational overhead for large resources when only a minuscule section needs modification.

After delving into this exploration, you should have a more enriched understanding of implementing the PUT method in JavaScript fetch requests, the pitfalls to evade, and efficiency considerations. Remember, prudent data management and an informed choice of method can significantly enhance the efficiency and dependability of your JavaScript application.

Deleting Resources with DELETE Requests

In the realm of web development, utilizing JavaScript's Fetch API is a crucial step towards effectively managing server resources. In this section, we specifically dive into using the DELETE request method, common mistakes and best practices to grasp the method's full potential.

Making a DELETE Request with Fetch

The syntax for a DELETE request with Fetch is straightforward and slightly less demanding than its counterparts. This is mainly because deleting a server resource doesn't typically require sending any additional data. Here, we'll illustrate a DELETE request that aims to discard a specific resource from the server.

fetch('https://example.com/resource-to-delete/1', {
    method: 'DELETE',
    headers: {
        'Content-type': 'application/json'
    }
})
.then(response => {
    if(response.ok) {
        console.log('Resource deleted successfully');
    } else {
        console.log('An error occurred while deleting the resource');
    }
})
.catch(error => console.log('Caught error: ', error));

In this snippet, we notify the server about our intention to delete the resource located at https://example.com/resource-to-delete/1. The object passed into the fetch function specifies the DELETE method and includes the necessary headers.

The server's response to a DELETE request is usually a status representation without a body. Alternatively, it could be an error indicating the failure of the operation.

Common Mistakes

The simplicity of DELETE requests doesn't fully shield developers from making mistakes, particularly those new to API interactions. Here are some common missteps:

Forgetting to Specify the DELETE Method: Omitting the DELETE method leads the fetch function to default to a GET request, resulting in an unintended behavior.

// Incorrect usage
fetch('https://example.com/resource-to-delete/1')

In the incorrect example above, the fetch function defaults to a GET request as no method was specified.

Neglecting Promise Handling: The fetch function returns a promise irrespective of the request's outcome. Ensure to manage these promises appropriately to prevent unforeseen results.

Unintended Server Response: Receiving unexpected outcomes could be down to an invalid URL or server-side limitations. Check the server's DELETE method support and possible restrictions before drawing conclusions.

Closing Remarks

Mastering DELETE requests with JavaScript's Fetch API broadens your ability to manipulate server data and effectively interact with APIs on a deeper level. It's essential to be familiar with the server's properties, handle promises correctly, and correctly define Fetch requests. This entails explicitly indicating the DELETE method and setting up appropriate headers.

Developing software is an iterative process where mistakes are commonplace. The most important aspect here is to learn from each mistake and steadily integrate better practices into your coding routine. Keep programming and keep learning!

Mastering Fetch Error Handling

One of the most common bugs that developers run into when working with JavaScript and fetch is improper error handling. See, the fetch API only rejects a Promise when it encounters a network error, such as a loss of connection, and not for faulty HTTP status codes like 404 or 500. It's a curious feature that often trips up even seasoned developers. This means that the response.ok property or response.status should always be checked in the .then() callback to properly handle HTTP errors.

Here's a real-world example of correct error handling:

fetch('https://my-api.com/data')
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json();
})
.then(data => {
    // Handle your data here
    console.log(data);
})
.catch((error) => {
    console.error('Error:', error);
});

In this sample code, the response.ok boolean is checked after the fetch request. If the response was not okay (an HTTP status code of 200-299), an error is thrown with the HTTP status code. This error will be caught in the .catch() callback where it can be handled appropriately.

It's also worth mentioning that checking response.ok or response.status is only part of the story. We should always have a .catch() at the end of our Promise chain to handle any errors that might crop up while our Promise is being resolved. This includes network errors or any errors that are thrown manually.

On the topic of .catch(), it's a common mistake to place the .catch() callback immediately after the fetch request. While it will indeed catch any network errors, it won't catch any errors that occur while the Promise is being resolved. Therefore, .catch() should always be placed at the end of the Promise chain:

Incorrect:

fetch('https://my-api.com/data')
.catch((error) => {
    console.error('Error:', error);
})
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json();
})

Correct:

fetch('https://my-api.com/data')
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json();
})
.catch((error) => {
    console.error('Error:', error);
});

To spot the subtle error in the first example, consider that when you place the .catch() handler immediately after the fetch(), it only catches errors that occur during the fetch() itself; it won't catch any errors that might occur while the response is being processed. Therefore, always make sure that your error handling is in the right place.

Being aware of these peculiarities can save a lot of debugging time, and hopefully this analysis helps you to create more robust error-handling code when working with fetch. Happy coding!

Advanced Use of Fetch – Headers, Params, and Async/Await

Advanced Fetch Usage: Headers, Params, and Async/Await

In this section, we will explore the advanced usage of Fetch, focusing on the manipulation of headers, managing parameters, as well as leveraging the async/await syntax to streamline your code.

Handling Fetch Headers

Headers provide a powerful way to control the behavior of an HTTP request and response. With Fetch, you can specify headers to define or overwrite various characteristics of the request or response.

Consider the following code block:

const headers = new Headers();
headers.append('Content-Type', 'application/json');
fetch('https://reqres.in/api/users/2', { headers: headers })
    .then(response => response.json())
    .then(data => console.log(data));

Here we created a new Headers object and appended a 'Content-Type' header with the value 'application/json'. The fetch request then uses these headers.

In terms of performance and complexity, manipulating headers doesn't introduce significant overhead. However, it does add to the complexity of the code which could compromise readability if not well-organized and commented.

Managing Fetch Params

Parameters are a core aspect of HTTP requests, especially when dealing with GET requests. Fetch allows us to include parameters in the URL of the request.

Consider the example below:

const url = new URL('https://reqres.in/api/users');
url.searchParams.append('page', 2);
fetch(url)
    .then(response => response.json())
    .then(data => console.log(data));

In the above code block, we declare a URL object and append a search parameter named 'page' with the value 2 to it. When fetch executes, it will request the specified URL with the appended parameters. While including parameters marginally increases the complexity of the request, it is a key tool for controlling server-side data manipulation and retrieval.

Streamlining with Async/Await

Fetch API returns promises, and modern JavaScript provides us with the async/await syntax to handle these promises in a clean, readable way. Let's consider an example:

async function getFetchData() {
    try {
        const response = await fetch('https://reqres.in/api/users/2');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.log(error);
    }
}
getFetchData();

In the above code, we declare an asynchronous function getFetchData(). Inside the function, we put our fetch statement inside a try/catch block, allowing us to gracefully handle any errors that arise. By prefixing our fetch with the await keyword, execution of the function pauses until the promise is resolved, simplifying the syntax significantly. When executed correctly, using async/await can lead to better-structured, easier-to-understand code.

So, when should we use these advanced features of Fetch? Take a moment to reflect on this. Can you envision scenarios in your current or recent projects where manipulating headers, managing parameters, or using async/await could improve not only the performance and scalability of your JavaScript projects but also the readability and maintainability of your code?

While these features introduce additional complexity, they open up a vast array of possibilities to deliver efficient and robust network requests in your web applications. Remember, the art of programming is about finding the right balance between simplicity and functionality. How will you maintain this balance in your projects?

Summary

The article "Making GET, POST, PUT, and DELETE requests with Fetch" delves into the Fetch API and its usage in handling different types of HTTP requests in JavaScript. The author provides step-by-step explanations and practical examples for each type of request, including GET, POST, PUT, and DELETE. The article covers topics such as error handling, performance issues, and best practices in using the Fetch API. It also explores advanced concepts like manipulating headers, managing parameters, and using async/await syntax.

Key takeaways from the article include understanding the basics of the Fetch API and its promise-based architecture, learning how to make GET requests to fetch data from APIs, understanding the nuances of making POST requests to create new resources on the server, grasping the usage of PUT requests to update existing resources, and learning how to handle DELETE requests to remove resources from the server. Additionally, the article highlights the importance of proper error handling, efficient network requests, and code readability.

A challenging task for the reader could be to create a web application that interacts with a RESTful API using the Fetch API. The task would involve implementing functionality to retrieve data from the API using GET requests, create new resources on the server using POST requests, update existing resources using PUT requests, and delete resources using DELETE requests. The reader would need to handle error scenarios, manage headers and parameters, and make efficient use of the Fetch API to ensure smooth and reliable communication with the API.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers