Created
November 7, 2025 20:07
-
-
Save Juan-Severiano/08a9d5d285323001d387c8f29013dbfa 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
| import { env } from "@/config/env"; | |
| const DEFAULT_HEADERS: Record<string, string> = { | |
| Accept: "application/json", | |
| }; | |
| export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; | |
| export type QueryParams = Record< | |
| string, | |
| string | number | boolean | null | undefined | |
| >; | |
| export interface HttpRequestConfig { | |
| headers?: Record<string, string>; | |
| query?: QueryParams; | |
| signal?: AbortSignal; | |
| credentials?: RequestCredentials; | |
| } | |
| export interface HttpRequestOptions<TBody> extends HttpRequestConfig { | |
| method: HttpMethod; | |
| body?: TBody; | |
| } | |
| export class HttpError<T = unknown> extends Error { | |
| readonly status: number; | |
| readonly data: T; | |
| constructor(status: number, message: string, data: T) { | |
| super(message); | |
| this.status = status; | |
| this.data = data; | |
| } | |
| } | |
| let userToken: string | null = null; | |
| export const setHttpClientUserToken = (token: string | null) => { | |
| userToken = token; | |
| }; | |
| export const revalidateApiAccessToken = (token: string | null) => { | |
| setHttpClientUserToken(token); | |
| }; | |
| const FALLBACK_BASE_URL = "http://localhost"; | |
| const normalizedBaseUrl = (env.VITE_API_URL || FALLBACK_BASE_URL).replace( | |
| /\/$/, | |
| "" | |
| ); | |
| const BASE_URL = `${normalizedBaseUrl}`.replace(/\/$/, ""); | |
| const toSearchParams = (query?: QueryParams) => { | |
| if (!query) { | |
| return ""; | |
| } | |
| const params = new URLSearchParams(); | |
| Object.entries(query).forEach(([key, value]) => { | |
| if (value === undefined || value === null) { | |
| return; | |
| } | |
| params.set(key, String(value)); | |
| }); | |
| const serialized = params.toString(); | |
| return serialized ? `?${serialized}` : ""; | |
| }; | |
| const buildUrl = (path: string, query?: QueryParams) => { | |
| const normalizedBase = BASE_URL.replace(/\/$/, ""); | |
| const isAbsolute = /^https?:\/\//i.test(path); | |
| if (isAbsolute) { | |
| const url = new URL(path); | |
| if (query) { | |
| const params = new URLSearchParams(url.search); | |
| Object.entries(query).forEach(([key, value]) => { | |
| if (value === undefined || value === null) { | |
| return; | |
| } | |
| params.set(key, String(value)); | |
| }); | |
| url.search = params.toString(); | |
| } | |
| return url.toString(); | |
| } | |
| const formattedPath = path.startsWith("/") ? path : `/${path}`; | |
| const base = normalizedBase || FALLBACK_BASE_URL; | |
| const url = new URL(`${base}${formattedPath}${toSearchParams(query)}`); | |
| return url.toString(); | |
| }; | |
| const isJsonBody = ( | |
| body: unknown | |
| ): body is Record<string, unknown> | unknown[] => | |
| body !== null && | |
| typeof body === "object" && | |
| !(body instanceof FormData) && | |
| !(body instanceof Blob); | |
| async function request<TResponse, TBody = unknown>( | |
| path: string, | |
| { | |
| method, | |
| body, | |
| headers, | |
| query, | |
| signal, | |
| credentials, | |
| }: HttpRequestOptions<TBody> | |
| ): Promise<TResponse> { | |
| const url = buildUrl(path, query); | |
| const mergedHeaders = new Headers({ | |
| ...DEFAULT_HEADERS, | |
| ...headers, | |
| }); | |
| if (userToken && !mergedHeaders.has("Authorization")) { | |
| mergedHeaders.set("Authorization", `Bearer ${userToken}`); | |
| } | |
| const config: RequestInit = { | |
| method, | |
| signal, | |
| credentials, | |
| headers: mergedHeaders, | |
| }; | |
| if (body !== undefined) { | |
| if (body instanceof FormData || body instanceof Blob) { | |
| config.body = body as BodyInit; | |
| } else if (isJsonBody(body)) { | |
| mergedHeaders.set("Content-Type", "application/json"); | |
| config.body = JSON.stringify(body); | |
| } else { | |
| config.body = body as BodyInit; | |
| } | |
| } | |
| try { | |
| const response = await fetch(url, config); | |
| const contentType = response.headers.get("content-type") ?? ""; | |
| let data: unknown = null; | |
| if (contentType.includes("application/json")) { | |
| data = await response.json(); | |
| } else if (contentType.includes("text/")) { | |
| data = await response.text(); | |
| } | |
| if (!response.ok) { | |
| const errorMessage = | |
| typeof data === "object" && data && "message" in data | |
| ? String((data as { message: unknown }).message) | |
| : response.statusText || "HTTP request failed"; | |
| throw new HttpError(response.status, errorMessage, data); | |
| } | |
| return data as TResponse; | |
| } catch (error) { | |
| if (error instanceof HttpError) { | |
| throw error; | |
| } | |
| throw error; | |
| } | |
| } | |
| export interface HttpClient { | |
| get<T>(path: string, config?: HttpRequestConfig): Promise<T>; | |
| post<T, B = unknown>( | |
| path: string, | |
| body?: B, | |
| config?: HttpRequestConfig | |
| ): Promise<T>; | |
| put<T, B = unknown>( | |
| path: string, | |
| body?: B, | |
| config?: HttpRequestConfig | |
| ): Promise<T>; | |
| patch<T, B = unknown>( | |
| path: string, | |
| body?: B, | |
| config?: HttpRequestConfig | |
| ): Promise<T>; | |
| delete<T, B = unknown>( | |
| path: string, | |
| body?: B, | |
| config?: HttpRequestConfig | |
| ): Promise<T>; | |
| } | |
| const createHttpClient = (): HttpClient => ({ | |
| get: (path, config) => request(path, { method: "GET", ...(config ?? {}) }), | |
| post: (path, body, config) => | |
| request(path, { method: "POST", body, ...(config ?? {}) }), | |
| put: (path, body, config) => | |
| request(path, { method: "PUT", body, ...(config ?? {}) }), | |
| patch: (path, body, config) => | |
| request(path, { method: "PATCH", body, ...(config ?? {}) }), | |
| delete: (path, body, config) => | |
| request(path, { method: "DELETE", body, ...(config ?? {}) }), | |
| }); | |
| export const httpClient = createHttpClient(); | |
| export { createHttpClient }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment