Created
June 10, 2025 20:45
-
-
Save xettri/86524222053fce5d2351e7cef87a9df6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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