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:
Essential knowledge of JavaScript (arrays, functions, methods & objects)
Basic understanding of Application Programming Interfaces (APIs)
Basic knowledge of HTML/CSS
Basic understanding of JavaScript fetch API
A text editor (VS Code recommended)
A browser (Chrome recommended)
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
ormultipart/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.
PUT
request: UsePUT
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 samePUT
request multiple times will always produce the same result. The resource will be created if it doesn't exist or updated if it does.
DELETE
request: UseDELETE
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 aDELETE
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
: AnAbortController
object is created with asignal
property, which is passed to thefetch
request.controller.abort()
: The fetch request is canceled after 3 seconds by calling theabort()
method.Handling the abort: In the
.catch()
block, we check if the error is anAbortError
, 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 triggerscontroller.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 newAbortController
for any new request.AbortError
Handling: When afetch
request is aborted, it throws anAbortError
. 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 withReadableStream
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!