UNPKG

kypi

Version:

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

147 lines (145 loc) 5.55 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; }; return new Proxy({}, { 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; }); } }); } /** * 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 { post as _, ahead as a, aput as c, del as d, endpoint as f, patch as g, ky as h, aget as i, authed as l, head as m, TimeoutError as n, apatch as o, get as p, adel as r, apost as s, HTTPError as t, client as u, put as v };