Skip to content

Instantly share code, notes, and snippets.

@AlexCR97
Last active February 9, 2025 21:27
Show Gist options
  • Select an option

  • Save AlexCR97/709042f73f587e7a3e9c54727b40c2c1 to your computer and use it in GitHub Desktop.

Select an option

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.
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;
};
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