JavaScript Fetch API: A Complete Overview - Part Two

JavaScript Fetch API: A Complete Overview - Part Two

Master HTTP Requests Using JavaScript's Fetch API

In the first part of this article, we covered the basics of the Fetch API, including how to make GET requests and handle responses and errors. In this article, we’ll explore more advanced features and use cases of the Fetch API. We’ll look at how to make POST, PUT, and DELETE requests and handle form data.

Additionally, we’ll discuss important topics like request timeouts and best practices for making efficient and secure API calls in JavaScript. By the end, you'll be well-equipped to manage a wide range of network interactions in your applications.

Prerequisites

To get the most out of this article, you should have:

POST Request

A POST request in JavaScript using the fetch API lets you send data to a server, usually to create or submit resources like user data, form entries, or upload files. Unlike GET, which is used to retrieve data, POST is used when you want to send data to the server.

Example:

const postData = async () => {
  try {
    const response = await fetch('https://example.com/api/resource', {
      method: 'POST', // Specifies the HTTP method
      headers: {
        'Content-Type': 'application/json' // Specifies the type of content being sent
      },
      body: JSON.stringify({
        name: 'John Doe',
        email: 'john@example.com'
      }) // The payload, which is sent as a JSON string
    });

    if (!response.ok) {
      throw new Error('Network response was not ok');
    }

    const data = await response.json(); // Parse the response as JSON
    console.log('Success:', data); // Handle success
  } catch (error) {
    console.error('Error:', error); // Handle errors
  }
};
// Call the function
postData();

It's important to note that method: 'POST' indicates a POST request is being made, and the body field is used to send data, usually converted to JSON with JSON.stringify(). The await keyword is used before fetch and response.json() to ensure each operation finishes before moving on.

The try...catch block is used for error handling. Any errors, whether in the fetch operation or during parsing, are caught and managed in the catch block. Error handling is crucial when dealing with network requests because things can go wrong, such as server errors, network issues, or validation problems.

Sending Data to a Server

When you send data to a server using a POST request, you typically send it in one of these formats:

  • JSON (JavaScript Object Notation): This is the most common format used for web APIs, especially RESTful APIs. Typically, when you send data to an API, you use JSON payloads. JSON is lightweight and easy to parse, which makes it the preferred choice for data transmission between client and server. Before sending data, you need to convert your JavaScript object into a JSON string using JSON.stringify().

  • Form Data: This format is used when submitting HTML forms, usually as application/x-www-form-urlencoded or multipart/form-data.

Example of sending JSON data to a server:

In the previous example, we sent a JSON payload using the body of the fetch request. The Content-Type header is set to application/json to inform the server that the data being sent is JSON.

const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('email', 'john@example.com');

fetch('https://example.com/api/resource', {
  method: 'POST',
  body: formData // Send FormData object as the body
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));

In this case, FormData is used to create form-like key-value pairs that represent form fields and their values. This is especially useful when submitting forms that include file uploads along with other data, like text inputs or checkboxes.

By using FormData, you can easily handle both file uploads and regular form data in one request, without needing to manually convert the data into the right format. This makes it an essential tool for managing multipart form submissions.

PUT and DELETE Requests

The fetch API supports PUT and DELETE requests, which are used to update and delete resources on a server. PUT replaces an existing resource with new data, while DELETE completely removes the data. These methods are key parts of RESTful APIs, where each HTTP method serves a specific purpose.

When to Use PUT vs DELETE

Here’s a detailed explanation of PUT and DELETE requests using the fetch API, including when to use them and how to manage sending data and deleting resources.

  1. PUT request: Use PUT to update or completely replace an existing resource with new data. In RESTful APIs, PUT is used when you have the complete dataset to send and want to overwrite the resource.

    • Example: updating a user’s profile information or replacing a document.

    • Idempotent: PUT requests are idempotent, meaning that making the same PUT request multiple times will always produce the same result. The resource will be created if it doesn't exist or updated if it does.

  2. DELETE request: Use DELETE to remove a resource. This request instructs the server to delete the specified resource.

    • Example: deleting a user’s account or removing a product from a list.

    • Idempotent:Like PUT, DELETE requests are idempotent. Making a DELETE request multiple times on the same resource will consistently result in its deletion.

Sending Data with PUT Requests

When making a PUT request, you typically send the updated data with the request. The server uses this data to update the resource.

Example:

const updateData = async () => {
  const updatedUser = {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
    age: 28
  };

  try {
    const response = await fetch('https://example.com/api/users/1', {
      method: 'PUT', // Use the PUT method
      headers: {
        'Content-Type': 'application/json' // Specify that the data is JSON
      },
      body: JSON.stringify(updatedUser) // Convert the JavaScript object to a JSON string
    });

    if (!response.ok) {
      throw new Error('Failed to update resource');
    }

    const data = await response.json(); // Parse the response data
    console.log('Resource updated:', data);
  } catch (error) {
    console.error('Error:', error);
  }
};

// Call the function
updateData();

In this example, the fetch request uses the PUT method. The request body contains the new data to replace the existing resource, usually in JSON format, serialized with JSON.stringify(). The URL includes the resource ID (/users/1) to specify which resource to update.

Deleting Resources with DELETE Requests

A DELETE request removes a resource from the server. Unlike PUT, you typically don't need to send any data with a DELETE request. You only need the correct resource identifier, like an ID in the URL.

Example:

const deleteData = async () => {
  try {
    const response = await fetch('https://example.com/api/users/1', {
      method: 'DELETE' // Use the DELETE method
    });

    if (!response.ok) {
      throw new Error('Failed to delete resource');
    }

    console.log('Resource deleted successfully');
  } catch (error) {
    console.error('Error:', error);
  }
};

// Call the function
deleteData();

In the example above, the fetch method is set to DELETE. Notice there is no body because you usually don't need to send one for a DELETE operation. The URL identifies the resource to be deleted. Proper error handling ensures your application behaves correctly when facing issues like bad requests, missing resources, or server errors.

Aborting Fetch Requests

The AbortController interface lets you create a signal to cancel a fetch request. To do this, create an AbortController instance, pass its signal to the fetch request, and call the abort() function to cancel it.

For example:

const controller = new AbortController(); // Create an instance of AbortController
const signal = controller.signal; // Retrieve the signal

// Fetch request
fetch('https://example.com/api/resource', { signal })
  .then(response => response.json())
  .then(data => console.log('Success:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  });

// Abort the fetch request after 3 seconds
setTimeout(() => controller.abort(), 3000);

In this code snippet:

  • AbortController: An AbortController object is created with a signal property, which is passed to the fetch request.

  • controller.abort(): The fetch request is canceled after 3 seconds by calling the abort() method.

  • Handling the abort: In the .catch() block, we check if the error is an AbortError, indicating the request was intentionally canceled.

Handling Timeout and Abort Errors

By using AbortController with setTimeout(), you can create a timeout for fetch requests. If a request takes too long, you can cancel it to prevent delays or poor user experiences.

For example:

const fetchWithTimeout = async (url, timeout = 5000) => {
  const controller = new AbortController();
  const signal = controller.signal;

  // Set a timeout to abort the request
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, { signal });
    clearTimeout(timeoutId); // Clear the timeout if the request completes in time

    if (!response.ok) {
      throw new Error('Network response was not ok');
    }

    const data = await response.json();
    console.log('Success:', data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request timed out and was aborted');
    } else {
      console.error('Fetch error:', error);
    }
  }
};

// Use the function
fetchWithTimeout('https://example.com/api/resource', 5000); // 5-second

In this example:

  • The setTimeout() function triggers controller.abort() after the specified timeout duration (5 seconds in this example).

  • If the request finishes before the timeout, clearTimeout() is called to avoid aborting the request.

  • If the request is too slow and gets aborted, the error is caught, and a timeout-specific message is logged.

You can use the same AbortController to cancel multiple fetch requests simultaneously. This is helpful when you have several parallel requests that you might want to abort all at once.

const controller = new AbortController();
const signal = controller.signal;

// Fetch request 1
fetch('https://example.com/api/resource1', { signal })
  .then(response => response.json())
  .then(data => console.log('Resource 1:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request 1 aborted');
    }
  });

// Fetch request 2
fetch('https://example.com/api/resource2', { signal })
  .then(response => response.json())
  .then(data => console.log('Resource 2:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request 2 aborted');
    }
  });

// Abort both requests after 2 seconds
setTimeout(() => controller.abort(), 2000);

In this example, both requests (resource1 and resource2) use the same AbortController. By calling abort(), you can cancel both requests simultaneously.

Key Points About AbortController

  • Once a request is aborted, the signal cannot be reused. You must create a new AbortController for any new request.

  • AbortError Handling: When a fetch request is aborted, it throws an AbortError. Always handle this error specifically to distinguish between intentional aborts and other errors.

  • Abort Other Async Tasks: AbortController can also be used to stop other asynchronous tasks, not just fetch requests. For instance, it can work with ReadableStream or custom async operations that support aborting.

Aborting fetch requests enhances user experience by conserving resources when users navigate away, enabling timeout control to prevent blocking interactions, and avoiding stale data by canceling earlier requests during actions like search-as-you-type.

Real-World Use Cases of the Fetch API

The Fetch API is commonly used in modern web development for various networking tasks. Here are some typical real-world applications:

1. Fetching Data from RESTful APIs

The most common use of the Fetch API is to get data from RESTful APIs. Applications frequently need to load data from remote servers to display to users, such as user profiles, product lists, or news articles.

2. Sending Form Data with Fetch

It allows developers to send form data to a server, usually during user registration or login. This is typically done with POST requests, where the form data is serialized and included in the request body (e.g., submitting form data via the POST method).

3. Working with Third-Party APIs

The Fetch API is essential for working with third-party services like user authentication, payments, and other external APIs. These services often require sending and receiving JSON data, along with headers for authorization and other necessary configurations.

4. Social Media

Using the Fetch API, developers can easily interact with social media platforms by making API requests to post, retrieve, or modify data. Many platforms, such as Facebook, Twitter, Instagram, and LinkedIn, offer APIs that enable developers to programmatically engage with their services.

Best Practices for Making API Calls in JavaScript

  • Use async/await for clearer code and simpler error handling.

  • Gracefully handle errors with try…catch blocks.

  • Set necessary headers, like Content-Type and authorization tokens.

  • Avoid hardcoding URLs; use environment variables for easier maintenance.

  • Use AbortController to set timeouts and cancel long-running requests.

  • Implement pagination and lazy loading for large datasets to boost performance.

  • Cache API responses when suitable to reduce load times.

  • Use POST requests to transmit sensitive data to protect it from being exposed in the URL.

  • Prevent UI blocking by using loaders during API calls.

  • Control API call frequency with debouncing or throttling.

  • Adhere to third-party API rate limits and apply retry strategies.

  • Secure API calls with HTTPS and manage token expirations.

  • Optimize network traffic by sending only necessary data and enabling compression.

Conclusion

By following these best practices, you can ensure that your API interactions are efficient, secure, and maintainable. With proper error handling, security measures, and performance optimizations, you are well on your way to building reliable web applications that handle API requests effectively. Embracing these strategies opens up opportunities for creating robust and responsive applications that can thrive in today's digital landscape.

✨ Happy Coding!