Skip to content

Instantly share code, notes, and snippets.

@xettri
Created June 10, 2025 20:45
Show Gist options
  • Save xettri/86524222053fce5d2351e7cef87a9df6 to your computer and use it in GitHub Desktop.
Save xettri/86524222053fce5d2351e7cef87a9df6 to your computer and use it in GitHub Desktop.
// extended version of header for response header with toJSON feature
class ResponseHeader extends Headers {
#jsonHeader: Record<string, string> | null = null;
constructor(init?: HeadersInit) {
super(init);
}
toJSON(): Record<string, string> {
// if header already parsed then return cached value
if (this.#jsonHeader) return this.#jsonHeader;
const result: Record<string, string> = {};
this.forEach((value, key) => {
result[key] = value;
});
this.#jsonHeader = result;
return result;
}
}
export interface HTTPResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: ResponseHeader;
}
export interface HTTPError<T = any> extends Error {
config: RequestOptions;
code?: string;
request?: Request;
response?: HTTPResponse<T>;
}
export interface RequestOptions extends Omit<RequestInit, 'headers'> {
headers?: Record<string, string | number> | Headers;
}
function isJSONResponse(headers: Headers): boolean {
const contentType = headers.get('Content-Type') || '';
return contentType.includes('application/json');
}
function createHTTPError<T>(
message: string,
code?: string,
response?: HTTPResponse<T>,
): HTTPError<T> {
const error = new Error(message.trim()) as HTTPError<T>;
error.name = 'HTTPError';
error.code = code;
error.response = response;
return error;
}
export class HTTPClient {
#baseUrl?: string;
constructor(baseUrl?: string) {
if (baseUrl) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
this.#baseUrl = baseUrl;
}
}
#resolveUrl(url: string) {
if (url.startsWith('/')) {
url = url.slice(1);
}
return this.#baseUrl ? `${this.#baseUrl}/${url}` : url;
}
// request maker for http client
async #request<T = any>(
method: RequestInit['method'],
url: string,
data: unknown = null,
options: RequestOptions = {},
): Promise<HTTPResponse<T>> {
const headers = new Headers({ 'Content-Type': 'application/json' });
// parse header passed by client
if (options.headers) {
if (options.headers instanceof Headers) {
options.headers.forEach((value, key) => {
headers.set(key, value);
});
} else if (typeof options.headers === 'object') {
Object.entries(options.headers).forEach(([key, value]) => {
// in case of number conver into string as number is not
// valid as value of any header
if (typeof value === 'number') {
value = String(value);
}
headers.set(key, value);
});
}
}
const config: RequestInit = {
...options,
headers,
method,
};
// in case data is not string so convert in to string as
// in fetch data is passed in body as string only
if (data && typeof data !== 'string') {
config.body = JSON.stringify(data);
}
// create a request to sent in fetch
const request = new Request(this.#resolveUrl(url), config);
let response: Response;
try {
response = await fetch(request);
} catch (networkError: any) {
throw createHTTPError<T>(
`Network Error ${networkError.message}`,
'ERR_NETWORK',
);
}
let responseBody: any;
try {
responseBody = isJSONResponse(response.headers)
? await response.json()
: await response.text();
} catch {
responseBody = null;
}
const responseHeader = new ResponseHeader(response.headers);
const finalResponse: HTTPResponse<T> = {
data: responseBody as T,
headers: responseHeader,
status: response.status,
statusText: response.statusText,
};
if (!response.ok) {
throw createHTTPError<T>(
`Request failed with status code ${response.status}`,
`ERR_HTTP_${response.status}`,
finalResponse,
);
}
return finalResponse;
}
public get<T = any>(
url: string,
options?: RequestOptions,
): Promise<HTTPResponse<T>> {
return this.#request<T>('GET', url, null, options);
}
public post<T = any>(
url: string,
data: unknown,
options?: RequestOptions,
): Promise<HTTPResponse<T>> {
return this.#request<T>('POST', url, data, options);
}
public put<T = any>(
url: string,
data: unknown,
options?: RequestOptions,
): Promise<HTTPResponse<T>> {
return this.#request<T>('PUT', url, data, options);
}
public patch<T = any>(
url: string,
data: unknown,
options?: RequestOptions,
): Promise<HTTPResponse<T>> {
return this.#request<T>('PATCH', url, data, options);
}
public delete<T = any>(
url: string,
options?: RequestOptions,
): Promise<HTTPResponse<T>> {
return this.#request<T>('DELETE', url, null, options);
}
}
export default new HTTPClient();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment