DEV Community

Shamim Bin Nur
Shamim Bin Nur

Posted on • Updated on

Build your own Promise-Based HTTP Client like axios/fetch : Ohey

Introduction

Today, I'm excited to share the journey of creating my very own npm package: Ohey. Ohey is a promise-based HTTP client built on top of XMLHttpRequest. Whether you're new to web development or an experienced coder, understanding how Ohey works will give you insights into building and using HTTP clients in JavaScript. Let's dive into the details of how I built this package!

Why Ohey?

With numerous HTTP clients like Axios and Fetch, you might wonder why I decided to create Ohey. The primary motivation was to learn more about HTTP requests and promises by building something from scratch.

Setting Up the Project

First, I set up a new npm project. You might already know npm, it’s a package manager for JavaScript that allows you to manage your project's dependencies. Here’s how you can set up a new npm project

  1. Open your terminal and create a new directory for your project:

    mkdir ohey
    cd ohey
    
  2. Initialize a new npm project:

    npm init -y
    

This creates a package.json file, which holds the metadata for your project.

Writing the Ohey Function

The core functionality of Ohey lies in the single ohey function. This function takes an endpoint and an options object and returns a promise that resolves or rejects based on the outcome of the HTTP request. Here’s the complete code for Ohey:

function ohey (
  endpoint,
  {
    method = "GET",
    baseUrl = "",
    timeout = 0,
    body,
    headers = { "Content-Type": "application/json" }
  } = {}
) {

    // Concatenate the baseURL and endpoint
  const url = `${baseUrl}${endpoint}`;

    // Define a promise and return it.
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    if (timeout > 0) {
      setTimeout(() => {
        xhr.abort();
        reject(new Error("Request timed out"));
      }, timeout);
    }
        // Initialize an HTTP request
    xhr.open(method, url, true);

        // Etarate through header, extract keys and value.
    for (const [key, value] of Object.entries(headers)) {
      xhr.setRequestHeader(key, value);
    }

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        let responseType = "Text";

        try {
          responseType = "JSON";
          resolve({ data: JSON.parse(xhr.responseText), responseType, responseCode: xhr.status, method });
        } catch (error) {
          resolve({ data: xhr.responseText, responseType, responseCode: xhr.status, method });
        }
      } else {
        reject(new Error(`Request failed with status: ${xhr.status}`));
      }
    };

    xhr.onerror = () => {
      reject(new Error("Network error"));
    };

        // Send the request with the body parameter
    xhr.send(body);
  });
};

// Export the function as a module
module.exports = ohey;
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Code

Let's dive deeper into each part of the ohey function to understand how it works.

Function Declaration and Default Parameters

The ohey function is declared with two main parameters: endpoint and an options object with several default values.

function ohey (
  endpoint,
  {
    method = "GET",
    baseUrl = "",
    timeout = 0,
    body,
    headers = { "Content-Type": "application/json" }
  } = {}
) {
Enter fullscreen mode Exit fullscreen mode
  • endpoint: The specific API endpoint you want to hit (e.g., "/users").
  • method: The HTTP method to use (default is "GET").
  • baseUrl: The base URL for the request (e.g., "https://api.example.com").
  • timeout: The request timeout duration in milliseconds (default is 0, meaning no timeout).
  • body: The request payload, used primarily for POST, PUT, and PATCH requests.
  • headers: An object representing the request headers (default includes "Content-Type: application/json").

Constructing the URL

The full URL for the request is constructed by concatenating the baseUrl and the endpoint.

const url = `${baseUrl}${endpoint}`;
Enter fullscreen mode Exit fullscreen mode

Returning a Promise

The ohey function returns a Promise, which allows asynchronous operations to be handled more gracefully.

return new Promise((resolve, reject) => {
Enter fullscreen mode Exit fullscreen mode

Creating the XMLHttpRequest Object

An instance of XMLHttpRequest is created to handle the HTTP request.

const xhr = new XMLHttpRequest();
Enter fullscreen mode Exit fullscreen mode

Handling Timeout

If a timeout value is specified, a timer is set to abort the request if it exceeds the given duration.

if (timeout > 0) {
  setTimeout(() => {
    xhr.abort();
    reject(new Error("Request timed out"));
  }, timeout);
}
Enter fullscreen mode Exit fullscreen mode
  • xhr.abort(): Cancels the request.
  • reject: The promise is rejected with a "Request timed out" error.

Configuring the XMLHttpRequest

The HTTP method and URL are set using xhr.open. The true parameter indicates that the request is asynchronous.

xhr.open(method, url, true);
Enter fullscreen mode Exit fullscreen mode

Setting Request Headers

Custom headers are added to the request using a for...of loop to iterate over the headers object, extract the keys and values, and set them on the request header.

for (const [key, value] of Object.entries(headers)) {
  xhr.setRequestHeader(key, value);
}
Enter fullscreen mode Exit fullscreen mode

Handling the Response

The onload event is triggered when the request completes successfully. It checks the HTTP status code to determine if the request was successful. Any response code 200 to 299 typically means the request has hit the server successfully.

xhr.onload = () => {
  if (xhr.status >= 200 && xhr.status < 300) {
    let responseType = "Text";

    try {
      responseType = "JSON";
      resolve({ data: JSON.parse(xhr.responseText), responseType, responseCode: xhr.status, method });
    } catch (error) {
      resolve({ data: xhr.responseText, responseType, responseCode: xhr.status, method });
    }
  } else {
    reject(new Error(`Request failed with status: ${xhr.status}`));
  }
};
Enter fullscreen mode Exit fullscreen mode
  • xhr.status: The HTTP status code of the response.
  • resolve: If the request is successful, the promise is resolved with the response data.
    • responseType: Indicates whether the response is JSON or plain text.
    • responseCode: The HTTP status code.
    • method: The HTTP method used for the request.
  • JSON.parse: Attempts to parse the response as JSON. If parsing fails, the response is returned as plain text.

Handling Network Errors

The onerror event is triggered if there is a network error. The promise is rejected with a "Network error" message.

xhr.onerror = () => {
  reject(new Error("Network error"));
};
Enter fullscreen mode Exit fullscreen mode

Sending the Request

Finally, the request is sent using xhr.send, with the body parameter included for methods like POST.

xhr.send(body);
  });
}
Enter fullscreen mode Exit fullscreen mode

Exporting the Function

The ohey function is exported so it can be used in other modules.

module.exports = ohey;
Enter fullscreen mode Exit fullscreen mode

Publishing the Package

To share Ohey with the world, I published it to the npm registry. Here's how you can publish your own package by following these two steps.:

  1. Login to npm:

    npm login
    
  2. Publish the Package:

    npm publish
    

Note: Make sure there’s no other package available with the same name on the NPM registry.

And that’s it! Your package is now available on npm for others to install and use.

Conclusion

Understanding the code behind Ohey may give you a basic foundation for working with HTTP requests in JavaScript. By building this package, I aimed to create a simple yet functional HTTP client that leverages the flexibility of promises. I hope this breakdown helps you appreciate the inner workings of Ohey and inspires you to create your own projects! Happy coding!

Top comments (0)