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.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs