Last active
February 9, 2025 21:27
-
-
Save AlexCR97/709042f73f587e7a3e9c54727b40c2c1 to your computer and use it in GitHub Desktop.
Minimalist HTTP Client for TypeScript. Single file, no bloat, no dependencies, just copy and paste into your project.
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
| import { useHttpClient } from "./httpclient"; | |
| // Create a client with a base URL and default headers | |
| const client = useHttpClient({ | |
| baseUrl: "https://jsonplaceholder.typicode.com", | |
| headers: { | |
| "content-type": "application/json", | |
| }, | |
| }); | |
| // Send a GET request | |
| const getResponse = await client.get<User[]>("users"); | |
| if (getResponse.ok) { | |
| console.log(getResponse.content); | |
| } | |
| // Send a POST request | |
| const newUser: User = { id: 11, username: "johndoe" }; | |
| const postResponse = await client.post("users", newUser); | |
| if (postResponse.ok) { | |
| console.log(postResponse.content); | |
| } | |
| type User = { | |
| id: number; | |
| username: string; | |
| }; |
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
| export function useHttpClient(options?: HttpClientOptions): HttpClient { | |
| const contentTypeHeaders = [ | |
| "content-type", | |
| "content-Type", | |
| "Content-type", | |
| "Content-Type", | |
| "CONTENT-TYPE", | |
| ] as const; | |
| const clientOptions = options; | |
| return { | |
| send<T>(method: HttpMethod, url: string, options?: HttpRequestOptions) { | |
| return sendRequest<T>(method, url, clientOptions, options); | |
| }, | |
| get<T>(url: string, options?: HttpRequestOptions) { | |
| return sendRequest<T>("GET", url, clientOptions, options); | |
| }, | |
| getJson<T>( | |
| url: string, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>> { | |
| return sendJsonRequest<T>("GET", url, options); | |
| }, | |
| post<T>(url: string, body: unknown, options?: HttpRequestOptions) { | |
| return sendRequest<T>("POST", url, clientOptions, { ...options, body }); | |
| }, | |
| postFile<T>( | |
| url: string, | |
| key: string, | |
| file: File, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>> { | |
| const formData = new FormData(); | |
| formData.set(key, file); | |
| return sendFormDataRequest("POST", url, formData, options); | |
| }, | |
| postFormData<T>( | |
| url: string, | |
| form: FormData, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>> { | |
| return sendFormDataRequest("POST", url, form, options); | |
| }, | |
| postJson<T>( | |
| url: string, | |
| body: unknown, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>> { | |
| return sendJsonRequest<T>("POST", url, { ...options, body }); | |
| }, | |
| put<T>(url: string, body: unknown, options?: HttpRequestOptions) { | |
| return sendRequest<T>("PUT", url, clientOptions, { ...options, body }); | |
| }, | |
| putJson<T>( | |
| url: string, | |
| body: unknown, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>> { | |
| return sendJsonRequest<T>("PUT", url, { ...options, body }); | |
| }, | |
| delete<T>(url: string, options?: HttpRequestOptions) { | |
| return sendRequest<T>("DELETE", url, clientOptions, options); | |
| }, | |
| }; | |
| async function sendRequest<T>( | |
| method: HttpMethod, | |
| url: string, | |
| clientOptions: HttpClientOptions | undefined, | |
| requestOptions: HttpRequestOptions | undefined | |
| ): Promise<HttpResponse<T>> { | |
| const requestUrl = buildUrl( | |
| clientOptions?.baseUrl, | |
| url, | |
| requestOptions?.query | |
| ); | |
| const headers = joinHeaders([ | |
| clientOptions?.headers, | |
| requestOptions?.headers, | |
| ]); | |
| const response = await fetch(requestUrl, { | |
| method, | |
| headers, | |
| body: parseBody(requestOptions?.body, getContentType(headers)), | |
| }); | |
| return { | |
| content: await parseResponseContent(response), | |
| headers: toHttpHeaders(response.headers), | |
| ok: response.ok, | |
| status: response.status, | |
| statusText: response.statusText, | |
| url: response.url, | |
| }; | |
| function buildUrl( | |
| baseUrl: string | undefined, | |
| requestUrl: string, | |
| query: Record<string, string | undefined> | undefined | |
| ) { | |
| const urlParts = [baseUrl, requestUrl]; | |
| const queryString = buildQueryString(query); | |
| const finalUrl = urlParts.filter((url) => url !== undefined).join("/"); | |
| return queryString ? `${finalUrl}?${queryString}` : finalUrl; | |
| function buildQueryString(query: HttpQuery | undefined): string | null { | |
| if (query === undefined) { | |
| return null; | |
| } | |
| return Object.keys(query) | |
| .map((key) => { | |
| const value = query[key]; | |
| return value ? `${key}=${encodeURIComponent(value)}` : undefined; | |
| }) | |
| .filter((pair) => pair !== undefined) | |
| .join("&"); | |
| } | |
| } | |
| function parseBody( | |
| body: unknown, | |
| contentType: string | null | |
| ): BodyInit | null { | |
| if (body === undefined || body === null) { | |
| return null; | |
| } | |
| const normalizedContentType = contentType?.trim().toLowerCase() ?? ""; | |
| if (normalizedContentType.includes("application/json")) { | |
| return JSON.stringify(body); | |
| } | |
| return body as BodyInit; | |
| } | |
| function getContentType(headers: HttpHeaders): string | null { | |
| for (const key of contentTypeHeaders) { | |
| if (headers[key]) { | |
| return headers[key]; | |
| } | |
| } | |
| return null; | |
| } | |
| async function parseResponseContent<T>(response: Response): Promise<T> { | |
| const contentType = | |
| response.headers.get("content-type")?.trim().toLowerCase() ?? ""; | |
| if (contentType.includes("application/json")) { | |
| return await response.json(); | |
| } | |
| if (contentType.includes("text/html")) { | |
| return (await response.text()) as T; | |
| } | |
| if (contentType.includes("text/plain")) { | |
| return (await response.text()) as T; | |
| } | |
| return response.bytes() as T; | |
| } | |
| function toHttpHeaders(headers: Headers): HttpHeaders { | |
| const responseHeaders: HttpHeaders = {}; | |
| headers.forEach((value, key) => (responseHeaders[key] = value)); | |
| return responseHeaders; | |
| } | |
| } | |
| async function sendFormDataRequest<T>( | |
| method: HttpMethod, | |
| url: string, | |
| form: FormData, | |
| options: HttpRequestOptions | undefined | |
| ): Promise<HttpResponse<T>> { | |
| const clientOptionsCopy = { ...clientOptions }; | |
| tryRemoveContentTypeHeader(clientOptionsCopy.headers); | |
| tryRemoveContentTypeHeader(options?.headers); | |
| return sendRequest<T>(method, url, clientOptionsCopy, { | |
| ...options, | |
| body: form, | |
| }); | |
| function tryRemoveContentTypeHeader(headers: HttpHeaders | undefined) { | |
| if (headers) { | |
| for (const key of contentTypeHeaders) { | |
| delete headers[key]; | |
| } | |
| } | |
| } | |
| } | |
| async function sendJsonRequest<T>( | |
| method: HttpMethod, | |
| url: string, | |
| options: HttpRequestOptions | undefined | |
| ): Promise<HttpResponse<T>> { | |
| const headers = joinHeaders([ | |
| options?.headers, | |
| { "content-type": "application/json" }, | |
| ]); | |
| return sendRequest<T>(method, url, clientOptions, { ...options, headers }); | |
| } | |
| function joinHeaders(allHeaders: (HttpHeaders | undefined)[]): HttpHeaders { | |
| return Object.assign({}, ...allHeaders.filter(Boolean)); | |
| } | |
| } | |
| export type HttpClient = { | |
| send<T>( | |
| method: HttpMethod, | |
| url: string, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| get<T>(url: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>; | |
| getJson<T>( | |
| url: string, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| post<T>( | |
| url: string, | |
| body: unknown, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| postFile<T>( | |
| url: string, | |
| key: string, | |
| file: File, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| postFormData<T>( | |
| url: string, | |
| form: FormData, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| postJson<T>( | |
| url: string, | |
| body: unknown, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| put<T>( | |
| url: string, | |
| body: unknown, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| putJson<T>( | |
| url: string, | |
| body: unknown, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| delete<T>( | |
| url: string, | |
| options?: HttpRequestOptions | |
| ): Promise<HttpResponse<T>>; | |
| }; | |
| export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"; | |
| export type HttpHeaders = Record<string, string>; | |
| export type HttpQuery = Record<string, string | undefined>; | |
| export type HttpClientOptions = { | |
| readonly baseUrl?: string; | |
| readonly headers?: HttpHeaders; | |
| }; | |
| export type HttpRequestOptions = { | |
| readonly body?: unknown; | |
| readonly headers?: HttpHeaders; | |
| readonly query?: HttpQuery; | |
| }; | |
| export type HttpResponse<T> = { | |
| readonly content: T; | |
| readonly headers: HttpHeaders; | |
| readonly ok: boolean; | |
| readonly status: number; | |
| readonly statusText: string; | |
| readonly url: string; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment