JAVASCRIPT
Create a Reusable and Robust API Client for Fetch
Develop a generic API client class in JavaScript to centralize HTTP requests, manage headers, handle errors consistently, and streamline API integrations.
class HttpClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...defaultHeaders,
};
}
async #request(method, endpoint, data = null, customHeaders = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = { ...this.defaultHeaders, ...customHeaders };
const config = {
method: method,
headers: headers,
};
if (data) {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
let errorData = null;
try {
errorData = await response.json(); // Attempt to parse error details
} catch (e) {
// Not all error responses are JSON
errorData = await response.text();
}
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`
);
}
// Handle no content responses (e.g., 204 No Content)
if (response.status === 204) {
return null;
}
return await response.json();
} catch (error) {
console.error(`Request to ${url} failed:`, error);
throw error; // Re-throw to allow calling code to handle
}
}
get(endpoint, customHeaders = {}) {
return this.#request('GET', endpoint, null, customHeaders);
}
post(endpoint, data, customHeaders = {}) {
return this.#request('POST', endpoint, data, customHeaders);
}
put(endpoint, data, customHeaders = {}) {
return this.#request('PUT', endpoint, data, customHeaders);
}
delete(endpoint, customHeaders = {}) {
return this.#request('DELETE', endpoint, null, customHeaders);
}
// Helper to set or update default headers
setDefaultHeader(key, value) {
this.defaultHeaders[key] = value;
}
}
// Example Usage:
// const api = new HttpClient('https://api.example.com/v1');
// // Set an authorization token after login
// // api.setDefaultHeader('Authorization', `Bearer ${yourAuthToken}`);
// (async () => {
// try {
// // GET request
// const users = await api.get('/users');
// console.log('Users:', users.slice(0, 2));
// // POST request
// const newUser = await api.post('/users', { name: 'Alice', email: '[email protected]' });
// console.log('New User:', newUser);
// // PUT request
// const updatedUser = await api.put(`/users/${newUser.id}`, { email: '[email protected]' });
// console.log('Updated User:', updatedUser);
// // DELETE request (assuming newUser.id exists and can be deleted)
// // await api.delete(`/users/${newUser.id}`);
// // console.log('User deleted successfully');
// // Example of an API error
// // await api.get('/non-existent-endpoint');
// } catch (error) {
// console.error('Operation failed:', error.message);
// }
// })();
How it works: This snippet provides a `HttpClient` class that serves as a reusable and centralized client for making API requests using the `fetch` API. It encapsulates common logic such as setting a base URL, managing default and custom headers (e.g., `Content-Type`, `Authorization`), serializing request bodies, and parsing JSON responses. It also includes robust error handling, attempting to parse error details from the response and throwing a custom error for non-successful HTTP statuses, as well as handling 204 No Content responses. This client promotes code consistency, reduces boilerplate, and makes API interactions more manageable across an application.