Skip to content

Instantly share code, notes, and snippets.

@WORMSS
Last active March 15, 2022 12:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WORMSS/b035f10d015a7a9471c82f73482a8aaf to your computer and use it in GitHub Desktop.
Save WORMSS/b035f10d015a7a9471c82f73482a8aaf to your computer and use it in GitHub Desktop.
Mini Api Builder
type URLSearchParamsInit = ConstructorParameters<typeof URLSearchParams>[0];
type ApiFormatMap = {
arrayBuffer: ArrayBuffer;
text: string;
json: object;
formData: FormData;
blob: Blob;
stream: ReadableStream<Uint8Array>;
response: Response;
};
type ApiFormat = keyof ApiFormatMap;
type ApiResponseType<T extends ApiFormat> = {
responseType?: T;
};
type ApiQuery = {
query?: URLSearchParamsInit;
};
type ApiDefaults<T extends ApiFormat> = RequestInit & ApiResponseType<T> & ApiQuery;
type ApiCallOptions<T extends ApiFormat> = RequestInit & ApiResponseType<T> & ApiQuery;
type ApiBuilder<T extends ApiFormat> = {
[key: string]: ApiBuilder<T>;
(firstArg: string | number, ...extraPaths: (string | number)[]): ApiBuilder<T>;
(): Promise<ApiFormatMap[T]>;
<O extends ApiFormat = T>(callOptions: ApiCallOptions<O>): Promise<ApiFormatMap[O]>;
};
export function createApi<T extends ApiFormat = 'response'>(
url: string,
defaults?: ApiDefaults<T>,
): ApiBuilder<T> {
const internalTarget = (() => {}) as unknown as ApiBuilder<T>;
const responseModifier = (callOptions?: ApiCallOptions<ApiFormat>) => {
const responseType: ApiFormat | undefined = callOptions?.responseType ?? defaults?.responseType;
switch (responseType) {
case 'json':
return (res: Response) => res.json();
case 'text':
return (res: Response) => res.text();
case 'arrayBuffer':
return (res: Response) => res.arrayBuffer();
case 'formData':
return (res: Response) => res.formData();
case 'blob':
return (res: Response) => res.blob();
case 'stream':
return (res: Response) => res.body;
case 'response':
default:
return (res: Response) => res;
}
};
const parseUrl = (callOptions?: ApiCallOptions<ApiFormat>): string => {
const parsedUrl: URL = new URL(url);
if (!!defaults?.query) {
new URLSearchParams(defaults.query).forEach((value, key) =>
parsedUrl.searchParams.set(key, value),
);
}
if (!!callOptions?.query) {
new URLSearchParams(callOptions.query).forEach((value, key) =>
parsedUrl.searchParams.set(key, value),
);
}
return parsedUrl.toString();
};
const parseHeadersInit = (callHeaders?: HeadersInit): Headers | HeadersInit | undefined => {
if (!defaults?.headers) {
return callHeaders;
}
if (!callHeaders) {
return defaults.headers;
}
const headers = new Headers(defaults.headers);
for (const [name, value] of new Headers(callHeaders)) {
headers.set(name, value);
}
return headers;
};
const parseRequestInit = (callOptions?: ApiCallOptions<ApiFormat>): RequestInit | undefined => {
if (!defaults) {
if (!callOptions) return;
const { query: _1, responseType: _2, ...requestInit } = callOptions;
return requestInit;
}
if (!callOptions) {
const { query: _1, responseType: _2, ...requestInit } = defaults;
return requestInit;
}
const { query: _1, responseType: _2, ...requestInitOptions } = callOptions;
const { query: _3, responseType: _4, ...requestInitDefaults } = defaults;
const headers = parseHeadersInit(callOptions.headers);
return {
...requestInitDefaults,
...requestInitOptions,
headers,
};
};
const p = (url: string): ApiBuilder<T> =>
new Proxy(internalTarget, {
get(_target, key: string) {
return p(`${url}/${key}`);
},
apply(_target, _thisArg, args: (string | number | ApiCallOptions<ApiFormat>)[]) {
// () => Promise<T>
if (args.length === 0) {
const modifier = responseModifier();
return fetch(parseUrl(), parseRequestInit()).then(modifier);
}
// (callOptions) => Promise<T or O>
if (typeof args[0] === 'object' && args[0] !== null) {
const options: ApiCallOptions<ApiFormat> = args[0];
const modifier = responseModifier(options);
return fetch(parseUrl(options), parseRequestInit(options)).then(modifier);
}
// (...paths) => ApiBuilder<T>
return p(`${url}/${args.join('/')}`);
},
});
return p(url);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment