@zayne-labs/callapi 1.16.0 → 1.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <p align="center">
8
8
  <!-- <a href="https://deno.bundlejs.com/badge?q=@zayne-labs/callapi,@zayne-labs/callapi&treeshake=%5B*%5D,%5B%7B+createFetchClient+%7D%5D&config=%7B%22compression%22:%7B%22type%22:%22brotli%22,%22quality%22:11%7D%7D"><img src="https://deno.bundlejs.com/badge?q=@zayne-labs/callapi,@zayne-labs/callapi&treeshake=%5B*%5D,%5B%7B+createFetchClient+%7D%5D&config=%7B%22compression%22:%7B%22type%22:%22brotli%22,%22quality%22:11%7D%7D" alt="bundle size"></a> -->
9
9
  <a href="https://www.npmjs.com/package/@zayne-labs/callapi"><img src="https://img.shields.io/npm/v/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="npm version"></a>
10
- <a href="https://github.com/zayne-labs/callapi/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="license"></a>
10
+ <a href="https://github.com/zayne-labs/callapi/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="license"></a>
11
11
  <a href="https://www.npmjs.com/package/@zayne-labs/callapi"><img src="https://img.shields.io/npm/dm/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="downloads per month"></a>
12
12
  <a href="https://github.com/zayne-labs/callapi/graphs/commit-activity"><img src="https://img.shields.io/github/commit-activity/m/zayne-labs/callapi?style=flat&color=EFBA5F" alt="commit activity"></a>
13
13
  <a href="https://deepwiki.com/zayne-labs/callapi"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
@@ -29,7 +29,7 @@
29
29
 
30
30
  Fetch is too basic for real apps. You end up writing the same boilerplate: error handling, retries, deduplication, response parsing etc. CallApi handles all of that and practically more.
31
31
 
32
- **Drop-in replacement for fetch. Under 6KB. All kinds of convenience features. Zero dependencies.**
32
+ **Fetch-style API. ~7KB. All kinds of convenience features. Zero dependencies.**
33
33
 
34
34
  ```js
35
35
  import { callApi } from "@zayne-labs/callapi";
@@ -45,7 +45,7 @@ User spam-clicks a button? Handled. No race conditions.
45
45
 
46
46
  ```js
47
47
  const req1 = callApi("/api/user");
48
- const req2 = callApi("/api/user"); // Cancels req1 (can be configured to share it's response instead)
48
+ const req2 = callApi("/api/user"); // Cancels req1 (can be configured to share its response instead)
49
49
  ```
50
50
 
51
51
  ### Smart Response Parsing
@@ -58,17 +58,24 @@ const { data } = await callApi("/api/data"); // JSON? Parsed.
58
58
 
59
59
  ### Error Handling
60
60
 
61
- Structured errors make robust error handling trivial.
61
+ Errors are returned, not thrown, so every failure is handled in one place.
62
62
 
63
63
  ```js
64
+ import { isHTTPError, isValidationError } from "@zayne-labs/callapi/utils";
65
+
64
66
  const { data, error } = await callApi("/api/users");
65
67
 
66
- if (error) {
67
- console.log(error.name); // "HTTPError", "ValidationError"
68
- console.log(error.errorData); // Actual API response
68
+ if (isHTTPError(error)) {
69
+ console.log(error.errorData); // The API's error response body
70
+ }
71
+
72
+ if (isValidationError(error)) {
73
+ console.log(error.errorData); // The schema validation issues
69
74
  }
70
75
  ```
71
76
 
77
+ Prefer try/catch? Set `throwOnError: true`.
78
+
72
79
  ### Retries
73
80
 
74
81
  Supports exponential backoff and custom retry conditions.
@@ -102,9 +109,13 @@ const callMainApi = createFetchClient({
102
109
  });
103
110
 
104
111
  // Fully typed + validated
105
- const user = await callMainApi("/users/:id", {
112
+ const { data: user, error } = await callMainApi("/users/:id", {
106
113
  params: { id: 123 },
107
114
  });
115
+
116
+ if (!error) {
117
+ user.name; // string
118
+ }
108
119
  ```
109
120
 
110
121
  ### Hooks
@@ -129,21 +140,27 @@ const api = createFetchClient({
129
140
 
130
141
  Extend functionality with setup, hooks, and middleware.
131
142
 
132
- ```js
133
- const metricsPlugin = definePlugin({
143
+ ```ts
144
+ import { createFetchClient, type GetCallApiContext } from "@zayne-labs/callapi";
145
+ import { definePluginWithContext } from "@zayne-labs/callapi/utils";
146
+
147
+ // Types the metadata this plugin reads and writes
148
+ type MetricsContext = GetCallApiContext<{ Meta: { startTime?: number } }>;
149
+
150
+ const metricsPlugin = definePluginWithContext<MetricsContext>()({
134
151
  id: "metrics",
135
152
  name: "Metrics Plugin",
136
153
 
137
154
  setup: ({ options }) => ({
138
155
  options: {
139
156
  ...options,
140
- meta: { startTime: Date.now() },
157
+ meta: { ...options.meta, startTime: Date.now() },
141
158
  },
142
159
  }),
143
160
 
144
161
  hooks: {
145
162
  onSuccess: ({ options }) => {
146
- const duration = Date.now() - options.meta.startTime;
163
+ const duration = Date.now() - (options.meta?.startTime ?? Date.now());
147
164
 
148
165
  console.info(`Request took ${duration}ms`);
149
166
  },
@@ -213,10 +230,12 @@ const api = createFetchClient({
213
230
 
214
231
  - **TypeScript-first** - Full inference everywhere
215
232
  - **Familiar API** - If you know fetch, you know CallApi
216
- - **Actually small** - Zero dependencies and Under 6KB, unlike other 50kb libs in the wild
233
+ - **Actually small** - Zero dependencies and ~7KB (minified + brotli)
217
234
  - **Fast** - Built on native Web APIs
218
235
  - **Works everywhere** - Browsers, Node 18+, Deno, Bun, Cloudflare Workers
219
236
 
237
+ Coming from axios, ky or ofetch? See [how CallApi compares](https://zayne-labs-callapi.vercel.app/docs/comparisons).
238
+
220
239
  ## License
221
240
 
222
241
  MIT © [Ryan Zayne](https://github.com/ryan-zayne)
@@ -172,7 +172,7 @@ type DedupeOptions = {
172
172
  *
173
173
  * // URL and method only - ignore headers and body
174
174
  * const userData = callApi("/api/user/123", {
175
- * dedupeKey: (context) => `${context.options.method}:${context.options.fullURL}`
175
+ * dedupeKey: (context) => `${context.request.method}:${context.options.fullURL}`
176
176
  * });
177
177
  *
178
178
  * // Include specific headers in deduplication
@@ -226,7 +226,7 @@ type DedupeOptions = {
226
226
  * // Dynamic strategy based on request
227
227
  * const smartClient = createFetchClient({
228
228
  * dedupeStrategy: (context) => {
229
- * return context.options.method === "GET" ? "defer" : "cancel";
229
+ * return context.request.method === "GET" ? "defer" : "cancel";
230
230
  * }
231
231
  * });
232
232
  *
@@ -697,8 +697,6 @@ type SharedExtraOptions<TCallApiContext extends CallApiContext = DefaultCallApiC
697
697
  * Supports multiple authentication patterns:
698
698
  * - String: Direct authorization header value
699
699
  * - Auth object: Structured authentication configuration
700
- *
701
- * ```
702
700
  */
703
701
  auth?: AuthOption;
704
702
  /**
@@ -797,7 +795,10 @@ type SharedExtraOptions<TCallApiContext extends CallApiContext = DefaultCallApiC
797
795
  */
798
796
  customFetchImpl?: FetchImpl;
799
797
  /**
800
- * Enable debug mode for the request.
798
+ * Log development warnings to the console.
799
+ *
800
+ * Currently warns when the resolved URL is relative, which fails during server-side rendering
801
+ * unless an absolute `baseURL` is set.
801
802
  *
802
803
  * @default true
803
804
  */
@@ -808,7 +809,7 @@ type SharedExtraOptions<TCallApiContext extends CallApiContext = DefaultCallApiC
808
809
  * Can be a static string or a function that receives error context
809
810
  * to generate dynamic error messages based on the response.
810
811
  *
811
- * @default "Failed to fetch data from server!"
812
+ * @default "Request failed unexpectedly"
812
813
  *
813
814
  * @example
814
815
  * ```ts
@@ -1043,7 +1044,7 @@ type BaseCallApiExtraOptions<TBaseCallApiContext extends CallApiContext = Defaul
1043
1044
  * @example
1044
1045
  * ```ts
1045
1046
  * // Skip all automatic merging - full manual control
1046
- * const client = callApi.create((ctx) => ({
1047
+ * const client = createFetchClient((ctx) => ({
1047
1048
  * skipAutoMergeFor: "all",
1048
1049
  *
1049
1050
  * // Manually decide what to merge
@@ -1056,7 +1057,7 @@ type BaseCallApiExtraOptions<TBaseCallApiContext extends CallApiContext = Defaul
1056
1057
  * }));
1057
1058
  *
1058
1059
  * // Skip options merging - manual plugin/hook control
1059
- * const client = callApi.create((ctx) => ({
1060
+ * const client = createFetchClient((ctx) => ({
1060
1061
  * skipAutoMergeFor: "options",
1061
1062
  *
1062
1063
  * // Manually control which plugins to use
@@ -1070,7 +1071,7 @@ type BaseCallApiExtraOptions<TBaseCallApiContext extends CallApiContext = Defaul
1070
1071
  * }));
1071
1072
  *
1072
1073
  * // Skip request merging - manual request control
1073
- * const client = callApi.create((ctx) => ({
1074
+ * const client = createFetchClient((ctx) => ({
1074
1075
  * skipAutoMergeFor: "request",
1075
1076
  *
1076
1077
  * // Extra options still auto-merge (plugins, hooks, etc.)
@@ -1113,10 +1114,13 @@ type CallApiExtraOptions<TCallApiContext extends CallApiContext = DefaultCallApi
1113
1114
  /**
1114
1115
  * Array of instance-specific CallApi plugins or a function to configure plugins.
1115
1116
  *
1116
- * Instance plugins are added to the base plugins and provide functionality
1117
- * specific to this particular API instance. Can be a static array or a function
1118
- * that receives base plugins and returns the instance plugins.
1117
+ * A static array replaces the base plugins for this request. To keep them, pass a function
1118
+ * that receives the base plugins and returns the full list.
1119
1119
  *
1120
+ * @example
1121
+ * ```ts
1122
+ * plugins: ({ basePlugins }) => [...basePlugins, metricsPlugin]
1123
+ * ```
1120
1124
  */
1121
1125
  plugins?: TPluginArray | ((context: InferExtendPluginContext<TBasePluginArray>) => TPluginArray);
1122
1126
  /**
@@ -1239,37 +1243,31 @@ type InferMetaOption<TSchema extends CallApiSchema, TCallApiContext extends Call
1239
1243
  *
1240
1244
  * @example
1241
1245
  * ```ts
1242
- * const callMainApi = callApi.create({
1246
+ * const callMainApi = createFetchClient({
1243
1247
  * baseURL: "https://main-api.com",
1244
- * onResponseError: ({ response, options }) => {
1248
+ * onResponseError: ({ options }) => {
1245
1249
  * if (options.meta?.userId) {
1246
1250
  * console.error(`User ${options.meta.userId} made an error`);
1247
1251
  * }
1248
1252
  * },
1249
1253
  * });
1250
1254
  *
1251
- * const response = await callMainApi({
1252
- * url: "https://example.com/api/data",
1255
+ * await callMainApi("/api/data", {
1253
1256
  * meta: { userId: "123" },
1254
1257
  * });
1255
1258
  *
1256
1259
  * // Use case: Request tracking
1257
- * const result = await callMainApi({
1258
- * url: "https://example.com/api/data",
1259
- * meta: {
1260
- * requestId: generateId(),
1261
- * source: "user-dashboard",
1262
- * priority: "high"
1263
- * }
1260
+ * await callMainApi("/api/data", {
1261
+ * meta: {
1262
+ * requestId: generateId(),
1263
+ * source: "user-dashboard",
1264
+ * },
1264
1265
  * });
1265
1266
  *
1266
- * // Use case: Feature flags
1267
- * const client = callApi.create({
1268
- * baseURL: "https://api.example.com",
1269
- * meta: {
1270
- * features: ["newUI", "betaFeature"],
1271
- * experiment: "variantA"
1272
- * }
1267
+ * // Use case: Default metadata for every request from a client
1268
+ * const client = createFetchClient({
1269
+ * baseURL: "https://api.example.com",
1270
+ * meta: { experiment: "variantA" },
1273
1271
  * });
1274
1272
  * ```
1275
1273
  */
@@ -1535,11 +1533,6 @@ interface RetryOptions<TErrorData> {
1535
1533
  * @deprecated **WARNING**: This property is used internally to track retries. Please abstain from reading or modifying it.
1536
1534
  */
1537
1535
  readonly ["~retryAttemptCount"]?: number;
1538
- /**
1539
- * Use a valid `Retry-After` response header instead of the configured retry delay
1540
- * @default false
1541
- */
1542
- respectRetryAfter?: boolean;
1543
1536
  /**
1544
1537
  * Number of allowed retry attempts on HTTP errors
1545
1538
  * @default 0
@@ -1587,16 +1580,31 @@ type RetryManagerContext = {
1587
1580
  };
1588
1581
  //#endregion
1589
1582
  //#region src/refetch.d.ts
1590
- declare const refetchAttemptTrackerSymbol: unique symbol;
1591
1583
  interface RefetchOptions {
1592
1584
  /**
1593
- * Tracks if the refetching of the request should be attempted
1585
+ * Tracks the number of times the request has already been refetched internally
1594
1586
  * @internal
1595
- * @deprecated **WARNING**: This property is used internally to track retries. Please abstain from reading or modifying it.
1587
+ * @deprecated **WARNING**: This property is used internally to track refetches. Please abstain from reading or modifying it.
1588
+ */
1589
+ readonly ["~refetchAttemptCount"]?: number;
1590
+ /**
1591
+ * Maximum number of times `refetch()` can re-run the original request.
1592
+ *
1593
+ * Guards against infinite loops, e.g. when a token refresh keeps failing with `401`.
1594
+ * Once the limit is reached, `refetch()` does nothing.
1595
+ * Can be overridden for a single call with `refetch({ maxAttempts })`.
1596
+ *
1597
+ * @default 1
1596
1598
  */
1597
- [refetchAttemptTrackerSymbol]?: boolean;
1599
+ refetchAttempts?: number;
1598
1600
  }
1599
- type RefetchFn = () => void;
1601
+ type RefetchFnOptions = {
1602
+ /**
1603
+ * Overrides the `refetchAttempts` option for this `refetch()` call.
1604
+ */
1605
+ maxAttempts?: number;
1606
+ };
1607
+ type RefetchFn = (refetchOptions?: RefetchFnOptions) => void;
1600
1608
  type RefetchManagerResult = {
1601
1609
  handleRefetch: () => Promise<CallApiResultLoose<unknown, unknown>> | null;
1602
1610
  refetch: RefetchFn;
@@ -1917,4 +1925,4 @@ type InferMetaFromTag<TTaggedType, TFallback = never> = TTaggedType extends Cont
1917
1925
  type InferExtraOptionsFromTag<TTaggedType> = TTaggedType extends ContextTag<unknown, infer TCallApiContext extends CallApiContext> ? unknown extends TCallApiContext["InferredExtraOptions"] ? never : TCallApiContext["InferredExtraOptions"] : never;
1918
1926
  //#endregion
1919
1927
  export { PossibleValidationError as $, DefaultCallApiContext as $t, isHTTPErrorInstance as A, ValidationError as At, defineSchema as B, FallBackRouteSchemaKey as Bt, SuccessContext as C, CallApiResult as Ct, metaHelper as D, InstanceContext as Dt, extraOptionsHelper as E, GetBaseSchemaRoutes as Et, defineFallbackRouteSchema as F, CallApiSchema as Ft, toSearchParams as G, DedupeOptions as Gt, defineSchemaRoutes as H, FetchImpl as Ht, defineInstanceConfig as I, CallApiSchemaConfig as It, CallApiResultSuccessVariant as J, CommonRequestHeaders as Jt, CallApiResultErrorVariant as K, AnyString as Kt, defineMainSchema as L, InferSchemaInput as Lt, isValidationError as M, BaseCallApiSchemaAndConfig as Mt, isValidationErrorInstance as N, BaseCallApiSchemaRoutes as Nt, objectifyHeaders as O, Register as Ot, defineBaseConfig as P, BaseSchemaRouteKeyPrefixes as Pt, PossibleJavaScriptError as Q, Writeable as Qt, definePlugin as R, InferSchemaOutput as Rt, ResponseStreamContext as S, CallApiRequestOptions as St, RetryOptions as T, GetBaseSchemaConfig as Tt, toFormData as U, FetchMiddlewareContext as Ut, defineSchemaConfig as V, fallBackRouteSchemaKey as Vt, toQueryString as W, Middlewares as Wt, InferCallApiResult as X, NoInferUnMasked as Xt, GetResponseType as Y, DistributiveOmit as Yt, PossibleHTTPError as Z, UnionToIntersection as Zt, HooksOrHooksArray as _, BaseCallApiConfig as _t, GetCallApiContextRequired as a, Body as at, ResponseContext as b, CallApiExtraOptions as bt, MetaWithContextTag as c, HeadersOption as ct, PluginMiddlewares as d, InferInitURL as dt, DefaultMetaObject as en, ResponseTypeMap as et, PluginSetupContext as f, InferParamsFromRoute as ft, Hooks as g, AuthOption as gt, ErrorContext as h, ThrowOnErrorBoolean as ht, GetCallApiContext as i, ApplyURLBasedConfig as it, isJavascriptError as j, URLOptions as jt, isHTTPError as k, HTTPError as kt, CallApiPlugin as l, InferAllMainRouteKeys as lt, CallApiRequestOptionsForHooks as m, SerializableObject as mt, ContextTag as n, fetchSpecificKeys as nn, ResultModeType as nt, InferExtraOptionsFromTag as o, GetCurrentRouteSchema as ot, CallApiExtraOptionsForHooks as p, SerializableArray as pt, CallApiResultSuccessOrErrorVariant as q, CommonContentTypes as qt, ExtraOptionsWithContextTag as r, ApplyStrictConfig as rt, InferMetaFromTag as s, GetCurrentRouteSchemaKey as st, CallApiContext as t, DefaultPluginArray as tn, ResponseTypeType as tt, PluginHooks as u, InferAllMainRoutes as ut, RequestContext as v, BaseCallApiExtraOptions as vt, RefetchOptions as w, CallApiResultLoose as wt, ResponseErrorContext as x, CallApiParameters as xt, RequestStreamContext as y, CallApiConfig as yt, definePluginWithContext as z, InferSchemaResult as zt };
1920
- //# sourceMappingURL=callapi-context-NF0HXJwF.d.ts.map
1928
+ //# sourceMappingURL=callapi-context-Gh3YMTYJ.d.ts.map
@@ -1,4 +1,4 @@
1
- import { Bt as FallBackRouteSchemaKey, Vt as fallBackRouteSchemaKey, nn as fetchSpecificKeys, v as RequestContext } from "../callapi-context-NF0HXJwF.js";
1
+ import { Bt as FallBackRouteSchemaKey, Vt as fallBackRouteSchemaKey, nn as fetchSpecificKeys, v as RequestContext } from "../callapi-context-Gh3YMTYJ.js";
2
2
  //#region src/constants/defaults.d.ts
3
3
  export declare const extraOptionDefaults: Readonly<Readonly<{
4
4
  bodySerializer: {
@@ -12,7 +12,7 @@ export declare const extraOptionDefaults: Readonly<Readonly<{
12
12
  dedupeStrategy: "cancel";
13
13
  defaultHTTPErrorMessage: "Request failed unexpectedly";
14
14
  hooksExecutionMode: "parallel";
15
- respectRetryAfter: false;
15
+ refetchAttempts: 1;
16
16
  responseParser: (text: string, reviver?: (this: any, key: string, value: any) => any) => any;
17
17
  responseType: "json";
18
18
  resultMode: "all";
@@ -1,2 +1,2 @@
1
- import { U as fetchSpecificKeys, n as requestOptionDefaults, t as extraOptionDefaults, z as fallBackRouteSchemaKey } from "../constants-KCmRZnc7.js";
1
+ import { U as fetchSpecificKeys, n as requestOptionDefaults, t as extraOptionDefaults, z as fallBackRouteSchemaKey } from "../constants-PKigNOpK.js";
2
2
  export { extraOptionDefaults, fallBackRouteSchemaKey, fetchSpecificKeys, requestOptionDefaults };
@@ -713,7 +713,7 @@ const extraOptionDefaults = Object.freeze(defineEnum({
713
713
  dedupeStrategy: "cancel",
714
714
  defaultHTTPErrorMessage: "Request failed unexpectedly",
715
715
  hooksExecutionMode: "parallel",
716
- respectRetryAfter: false,
716
+ refetchAttempts: 1,
717
717
  responseParser: JSON.parse,
718
718
  responseType: "json",
719
719
  resultMode: "all",
@@ -729,4 +729,4 @@ const requestOptionDefaults = Object.freeze(defineEnum({ method: "GET" }));
729
729
  //#endregion
730
730
  export { defineSchemaRoutes as A, isArray as B, defineFallbackRouteSchema as C, definePluginWithContext as D, definePlugin as E, handleConfigValidation as F, isString as H, handleSchemaValidation as I, HTTPError as L, toQueryString as M, toSearchParams as N, defineSchema as O, getCurrentRouteSchemaKeyAndMainInitURL as P, ValidationError as R, defineBaseConfig as S, defineMainSchema as T, fetchSpecificKeys as U, isFunction as V, isHTTPError as _, getBody as a, isValidationError as b, getMethod as c, splitConfig as d, waitFor as f, objectifyHeaders as g, metaHelper as h, createTimeoutSignal as i, toFormData as j, defineSchemaConfig as k, getResolvedHeaders as l, extraOptionsHelper as m, requestOptionDefaults as n, getFetchImpl as o, getFullAndNormalizedURL as p, createCombinedSignal as r, getHeaders as s, extraOptionDefaults as t, omitKeys as u, isHTTPErrorInstance as v, defineInstanceConfig as w, isValidationErrorInstance as x, isJavascriptError as y, fallBackRouteSchemaKey as z };
731
731
 
732
- //# sourceMappingURL=constants-KCmRZnc7.js.map
732
+ //# sourceMappingURL=constants-PKigNOpK.js.map