UNPKG

kypi

Version:

Type-safe, ergonomic API client builder for TypeScript & React based on ky.

148 lines (146 loc) 5.49 kB
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 };