Building "Ohey": A Promise-Based HTTP Client like axios/fetch
Tags: webdev, javascript, npm, promise
Published: 2024-06-11 19:41:57

### 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`](https://developer.mozilla.org/en-US/docs/Web/API/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](https://www.npmjs.com/package/ohey)!

### Why Ohey?

With numerous HTTP clients like [Axios](https://axios-http.com/docs/intro) and [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), you might wonder why I decided to create Ohey. The primary motivation was to learn more about HTTP requests and [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) by building something from scratch.

### Setting Up the Project

First, I set up a new npm project. You might already know [npm](https://docs.npmjs.com/about-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:
    
    ```bash
    mkdir ohey
    cd ohey
    ```
    
2. Initialize a new npm project:
    
    ```bash
    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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves or rejects based on the outcome of the HTTP request. Here’s the complete code for Ohey:

```jsx
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;
```

### 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.

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

- **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`.

```jsx
const url = `${baseUrl}${endpoint}`;
```

### Returning a Promise

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

```jsx
return new Promise((resolve, reject) => {
```

### Creating the XMLHttpRequest Object

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

```jsx
const xhr = new XMLHttpRequest();
```

### Handling Timeout

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

```jsx
if (timeout > 0) {
  setTimeout(() => {
    xhr.abort();
    reject(new Error("Request timed out"));
  }, timeout);
}
```

- **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](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing).

```jsx
xhr.open(method, url, true);
```

### 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.

```jsx
for (const [key, value] of Object.entries(headers)) {
  xhr.setRequestHeader(key, value);
}
```

### Handling the Response

The `onload` event is triggered when the request completes successfully. It checks the [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) to determine if the request was successful. Any response code 200 to 299 typically means the request has hit the server successfully.

```jsx
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.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.

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

### Sending the Request

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

```jsx
xhr.send(body);
  });
}
```

### Exporting the Function

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

```jsx
module.exports = ohey;
```

### 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**:
    
    ```bash
    npm login
    ```
    
2. **Publish the Package**:
    
    ```bash
    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](https://www.npmjs.com/package/ohey), 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!