kypi
Version:
Type-safe, ergonomic API client builder for TypeScript & React based on ky.
148 lines (146 loc) • 5.49 kB
JavaScript
import ky, { HTTPError, TimeoutError } from "ky";
//#region src/index.ts
/**
* Creates an endpoint definition.
*
* @param method - HTTP method (e.g., 'get', 'post', etc.)
* @param url - URL path for the endpoint
* @param opts - Optional parameters, including `auth` to indicate if authentication is required
*/
const endpoint = (method, url, opts = {}) => ({
method,
url,
auth: opts.auth ?? false
});
/**
* Same as `endpoint`, but it creates an endpoint that requires authentication.
*
* Will use the provided `getToken` function to retrieve the token and add it to the `Authorization`
* header.
*/
const authed = (method, url) => endpoint(method, url, { auth: true });
/** Creates an HTTP GET endpoint. */
const get = (url, opts) => endpoint("get", url, opts);
/** Creates an HTTP POST endpoint. */
const post = (url, opts) => endpoint("post", url, opts);
/** Creates an HTTP PUT endpoint. */
const put = (url, opts) => endpoint("put", url, opts);
/** Creates an HTTP PATCH endpoint. */
const patch = (url, opts) => endpoint("patch", url, opts);
/** Creates an HTTP HEAD endpoint. */
const head = (url, opts) => endpoint("head", url, opts);
/** Creates an HTTP DELETE endpoint. */
const del = (url, opts) => endpoint("delete", url, opts);
/** Creates an authed HTTP GET endpoint. */
const aget = (url) => authed("get", url);
/** Creates an authed HTTP POST endpoint. */
const apost = (url) => authed("post", url);
/** Creates an authed HTTP PUT endpoint. */
const aput = (url) => authed("put", url);
/** Creates an authed HTTP PATCH endpoint. */
const apatch = (url) => authed("patch", url);
/** Creates an authed HTTP HEAD endpoint. */
const ahead = (url) => authed("head", url);
/** Creates an authed HTTP DELETE endpoint. */
const adel = (url) => authed("delete", url);
function interpolateUrl(url, params) {
if (!params) return url;
return url.replaceAll(/:(\w+)/g, (_, key) => {
if (params[key] === void 0) throw new Error(`Missing param: ${key}`);
return encodeURIComponent(params[key]);
});
}
function createDeferredKyCall(makeKyCall) {
let promise = null;
const getPromise = () => {
if (!promise) promise = makeKyCall();
return promise;
};
const handler = { get(_target, prop) {
if (prop === "then") return (...args) => getPromise().then(...args);
return (...args) => getPromise().then((resp) => {
const fn = resp[prop];
if (typeof fn === "function") return fn.apply(resp, args);
return fn;
});
} };
return new Proxy({}, handler);
}
/**
* Creates a client for the given endpoint group.
*
* @param options - Options for the API client.
* @param options.baseUrl - The base URL for the API
* @param options.getToken - Optional function to retrieve an authentication token
* @param options.endpoints - The endpoint group definition
* @param options.onError - Optional onError hook, will be called if the request throws an error
* @param options.kyInstance - Optional custom Ky instance to use, defaults to the default instance
*/
function client({ baseUrl, getToken, onError, endpoints, kyInstance = ky }) {
const build = (group) => {
const client$1 = {};
for (const [key, value] of Object.entries(group)) if ("method" in value) {
const endpoint$1 = value;
client$1[key] = (input, kyOptions) => {
const makeKyCall = async () => {
let params = void 0;
let body = void 0;
let query = void 0;
if (input && typeof input === "object") {
if ("params" in input) params = input.params;
if ("body" in input) body = input.body;
if ("query" in input) query = input.query;
if (!body && !kyOptions?.body && !kyOptions?.json) body = input;
} else if (input !== void 0) body = input;
const url = baseUrl + interpolateUrl(endpoint$1.url, params);
let kyopts = {
method: endpoint$1.method,
headers: {}
};
if (endpoint$1.auth) {
const token = getToken?.();
if (token) if (token instanceof Promise) {
const awaitedToken = await token;
kyopts.headers.Authorization = `Bearer ${awaitedToken}`;
} else kyopts.headers.Authorization = `Bearer ${token}`;
}
if (query) kyopts.searchParams = query;
if (endpoint$1.method === "get" || endpoint$1.method === "head") {
if (!query && input && typeof input === "object" && !("body" in input) && !("params" in input) && !("query" in input)) kyopts.searchParams = input;
} else if (body !== void 0) if (body instanceof FormData) kyopts.body = body;
else kyopts.json = body;
if (kyOptions) {
const mergeObj = (a, b) => {
const aObj = a && typeof a === "object" && !Array.isArray(a) ? a : void 0;
const bObj = b && typeof b === "object" && !Array.isArray(b) ? b : void 0;
if (aObj && bObj) return {
...aObj,
...bObj
};
if (b !== void 0) return b;
return a;
};
kyopts = {
...kyopts,
...kyOptions,
headers: {
...kyopts.headers,
...kyOptions.headers
},
searchParams: mergeObj(kyopts.searchParams, kyOptions.searchParams)
};
}
return kyInstance(url, kyopts).catch((error) => {
onError?.(error);
throw error;
});
};
return createDeferredKyCall(makeKyCall);
};
} else client$1[key] = build(value);
return client$1;
};
return build(endpoints);
}
//#endregion
export { HTTPError, TimeoutError, adel, aget, ahead, apatch, apost, aput, authed, client, del, endpoint, get, head, ky, patch, post, put };