CallApi Fetch is an extra-lightweight wrapper over fetch that provides quality of life improvements beyond the bare fetch api, while keeping the API familiar.
It takes in a url and a request options object, just like fetch, but with some additional options to make your life easier. Check out the API Reference for more details.
# npm
npm install @zayne-labs/callapi
# pnpm
pnpm add @zayne-labs/callapi
Then you can use it by importing it in your JavaScript file.
import { callApi } from "@zayne-labs/callapi";
You can import callApi directly into JavaScript through a CDN.
To do this, you first need to set your script
's type to module
, then import callApi
.
<script type="module">
import { callApi } from "https://esm.run/@zayne-labs/callapi";
</script>
<!-- Locked to a specific version -->
<script type="module">
import { callApi } from "https://esm.run/@zayne-labs/callapi@0.3.2";
</script>
You can use callApi just like a normal fetch
function. The only difference is you don't have to write a response.json
or response.text
, you could just destructure the data and error directly.
This also means that all options for the native fetch options are supported, and you can use the same syntax to send requests.
const { data, error } = await callApi("url", fetchOptions);
You also have access to the response object itself via destructuring:
const { data, error, response } = await callApi("url", fetchOptions);
To see how to use callApi with typescript for extra autocomplete convenience, visit the Typescript section
CallApi supports all response types offered by the fetch api like json
, text
,blob
,formData
etc, so you don't have to write response.json()
, response.text()
, response.formData()
etc.
You can configure the response type you prefer by passing in the responseType
option and setting it to the form you want the data from the response to be in. By default it's set to json
.
// Json (default)
const { data } = await callApi("url", { responseType: "json" });
// Text
const { data } = await callApi("url", { responseType: "text" });
// Blob, etc
const { data } = await callApi("url", { responseType: "blob" });
// Doing this in fetch would imply:
const response = await fetch("some-url");
const data = await response.json(); // Or response.text() or response.blob() etc
CallApi provides a unified error handling through an error
object, which captures both HTTP errors (errors coming as a response from the api) and standard JavaScript errors.
The error
object contains the following properties:
name
: A string indicating the type of error (e.g., 'TypeError', 'SyntaxError', 'HTTPError').message
: The error message describing what went wrong.errorData
: The error data, which can be an error response from the API or a standard JavaScript error object.
For HTTP errors:
name
is set to "HTTPError"- An additional
errorData
property contains the error response data from the API.
For non-HTTP errors (e.g., TypeError, SyntaxError):
name
reflects the specific JavaScript error type (i.e., 'TypeError', 'SyntaxError')- The
errorData
property contains the original JavaScript Error object.
This structure allows you to easily identify and handle different types of errors that may occur during API calls.
const { data, error } = await callApi("some-url");
console.log(error.name);
console.log(error.message);
// Will reference the would contain the parsed error response data if it is an HTTPError, else it would just reference the corresponding js Error object
console.log(error.errorData);
For extra convenience with typescript, visit the Typescript section
CallApi
uses an internal request management system to ensure that only the most recent request to a given URL is processed, hence preventing the dreaded race conditions.
How this feature Works in detail:
- When a request is made,
callApi
internals check if there's an ongoing request to the same URL. - If a pending request exists, it's automatically cancelled.
- Then the new request is sent.
- This process is repeated until it is certain that only the latest request to that same URL is allowed to proceed.
Key takeaways:
- Automatic Cancellation: When multiple requests are made to the same URL in quick succession,
callApi
automatically cancels any pending previous requests, allowing only the latest request to proceed. - Race Condition Prevention: This mechanism eliminates race conditions that can occur when rapid, successive API calls are made, such as during fast typing in a search input, button clicks, etc.
- Ideal for React Hooks: This feature is particularly useful when
callApi
is used within React's useEffect hook or similar scenarios where component updates might trigger multiple API calls. - Configurable: If you prefer to handle request management differently, you can disable this feature by setting { cancelRedundantRequests: false } in the fetch options. No pressure 👌.
- Manual Cancellation: You can manually cancel requests to a specific URL using the cancel method attached to
callApi
. You can also pass an abort controller signal tocallApi
(just like with fetch) as an option and abort the request when you want to.
Using callApi.cancel
:
callApi("some-url");
callApi.cancel("some-url");
Using AbortController
:
const controller = new AbortController();
callApi("some-url", { signal: controller.signal });
controller.abort();
You can add query
object as an option and callApi will create a query string for you automatically.
callApi("some-url", {
query: {
param1: "value1",
param2: "to encode",
},
});
// The above request can be written in Fetch like this:
fetch("url?param1=value1¶m2=to%20encode");
CallApi
sets Content-Type
automatically depending on your body
data. Supported data types for this automatic setting include:
- Object
- Query Strings
- FormData
If you pass in an object
, callApi will set Content-Type
to application/json
. It will also JSON.stringify
your body so you don't have to do it yourself.
callApi.post("some-url", {
body: { message: "Good game" },
});
// The above request is equivalent to this
fetch("some-url", {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Good game" }),
});
If you pass in a string, callApi will set Content-Type
to application/x-www-form-urlencoded
.
CallApi
also contains a toQueryString
method that can help you convert objects to query strings so you can use this option easily.
import { toQueryString } from "@zayne-labs/callapi";
callApi("some-url", {
method: "POST",
body: toQueryString({ message: "Good game" }),
});
// The above request is equivalent to this
fetch("some-url", {
method: "post",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "message=Good%20game",
});
If you pass in a FormData, callApi will let the native fetch
function handle the Content-Type
. Generally, this will use multipart/form-data
with the default options.
const data = new FormData(form.elements);
callApi("some-url", { body: data });
If you provide callApi with an auth
property, it will conveniently generate an Authorization Header for you.
If you pass in a string
(commonly for tokens) , it will generate a Bearer Auth.
But if you pass in an object
, you would have two options to chose from:
- Use
bearer
option if you want to generate a Bearer Auth Header. - Use
token
if you want to generate a Token Auth Header.
Passing a string:
callApi("some-url", { auth: "token12345" });
The above request can be written in Fetch like this:
fetch("some-url", {
headers: { Authorization: `Bearer token12345` },
});
Passing an object:
// For Bearer Auth
callApi("some-url", { auth: { bearer: "token12345" } });
// Or
// For Token Auth
callApi("some-url", { auth: { token: "token12345" } });
The above requests can be written in Fetch like this:
// For Bearer Auth
fetch("some-url", {
headers: { Authorization: `Bearer token12345` },
});
// Or
// For Token Auth
fetch("some-url", {
headers: { Authorization: `Token token12345` },
});
You can create an instance of callApi
with predefined options. This is super helpful if you need to send requests with similar options
.
Things to note:
- All
options
that can be passed tocallApi
can also be passed tocallApi.create
. - Any options passed to
callApi.create
will be applied to all requests made with the instance. - If you pass a similar
options
property to the instance, the instance's options will take precedence.
import { callApi } from "@zayne-labs/callapi";
// Creating the instance, with some base options
const callAnotherApi = callApi.create({
timeout: 5000,
baseURL: "https://api.example.com"
});
// Using the instance
const { data, error } = await callAnotherApi("some-url");
// Overriding the timeout option (all base options can be overridden via the instance)
const { data, error } = await callAnotherApi("some-url", {
timeout: 10000,
});
You could also use the createFetchClient
function to create an instance, if you don't want to use callApi.create
.
import { createFetchClient } from "@zayne-labs/callapi";
const callApi = createFetchClient({
timeout: 5000,
baseURL: "https://api.example.com"
});
By default callApi supports all response types offered by the fetch api like json
, text
,blob
etc, so you don't have to write response.json()
, response.text()
or response.blob()
.
But if you want to parse a response with a custom function other than the default JSON.parse
, you can pass a custom parser function to the responseParser
option.
const { data, error } = await callApi("url", {
responseParser: customResponseParser,
});
Or even better, provide it as a callApi base option.
const callAnotherApi = callApi.create({
responseParser: customResponseParser,
});
You could also provide a custom serializer/stringifier, other the default JSON.stringify
, for objects passed to the reuqest body via the bodySerializer
option.
const callAnotherApi = callApi.create({
bodySerializer: customBodySerializer,
});
CallApi also provides a responseValidator
option, which could pass in a function that would validate the data returned from the server.
A good use case for this would to pass in, for instance, a zod schema parse/safeParse function to validate the data.
If your parser function throws an error, and have the throwOnError
option set to true
, you will expected to check and handle the errors in a catch block.
But, if throwOnError
is set to false
(default), it will just return the error object as usual.
const callMainApi = await callApi.create({
responseValidator: zodSchema.parse, // or zodSchema.safeParse or any other validator you wish to use
});
Providing interceptors to hook into lifecycle events of a callApi
call is possible.
These interceptors can be either asynchronous or synchronous.
Note: You might want to use callApi.create
to set shared interceptors
onRequest
is called function that is called just before the request is made, allowing you to modify the request or perform additional operations.
await callApi("/api", {
onRequest: ({ request, options }) => {
// Log request
console.log(request, options);
// Do other stuff
},
});
onRequestError
when an error occurs during the fetch request and it fails, providing access to the error object, request details and fetch options.
await callApi("/api", {
onRequestError: ({ request, options, error }) => {
// Log error
console.log("[fetch request error]", request, error);
},
});
onResponse
will be called when a successful response is received, providing access to the response, request details and fetch options.
The response object here contains all regular fetch response properties, plus a data
property, which contains the parsed response body.
await callApi("/api", {
onResponse: ({ request, response, options }) => {
// Log response
console.log(request, response.status, response.data);
// Do other stuff
},
});
onResponseError
is called when an error response (status code >= 400) is received from the api, providing access to the response object, request details, and fetch options used.
The response object here contains all regular fetch response properties, plus an errorData
property, which contains the parsed response error json response, if the server returns one.
This to note for this interceptor to be triggered:
- The
response.ok
property will befalse
. - The
response.status
property will be >= 400. - Essentially only error http responses return by the api will trigger this interceptor.
- It won't trigger for error responses not from the api, like network errors, syntax errors etc. Handle those in
onRequestError
interceptor.
The response object here contains all regular fetch response properties, plus an errorData
property, which contains the parsed response error json response, if the server returns one.
This example uses a shared interceptor for all requests made with the instance.
const callAnotherApi = callApi.create({
onResponseError: ({ response, request, options }) => {
// Log error response
console.log(request, response.status, response.errorData);
// Perform action on various error conditions
if (response.status === 401) {
actions.clearSession();
}
if (response.status === 429) {
toast.error("Too may requests!");
}
if (response.status === 403 && response.errorData?.message === "2FA is required") {
toast.error(response.errorData?.message, {
description: "Please authenticate to continue",
});
}
if (response.status === 500) {
toast.error("Internal server Error!");
}
},
});
CallApi
support retries for requests if an error happens and if the response status code is included in retryStatusCodes
list:
Default Retry status codes:
408
- Request Timeout409
- Conflict425
- Too Early429
- Too Many Requests500
- Internal Server Error502
- Bad Gateway503
- Service Unavailable504
- Gateway Timeout
You can specify the amount of retries and delay between them using retry
and retryDelay
options and also pass a custom array of codes using retryStatusCodes
option.
You can also specify which methods should be retried by passing in a custom retryMethods
array.
The default for retry
is 0
retries.
The default for retryDelay
is 0
ms.
The default for retryMethods
is ["GET", "POST"]
.
await callApi("http://google.com/404", {
retry: 3,
retryDelay: 500, // ms
retryStatusCodes: [404, 502, 503, 504], // custom status codes for retries
retryMethods: ["POST", "PUT", "PATCH", "DELETE"], // custom methods for retries
});
You can specify timeout
in milliseconds to automatically abort a request after a timeout (default is disabled).
await callApi("http://google.com/404", {
timeout: 3000, // Timeout after 3 seconds
});
You can throw an error on all errors (including http errors) by passing throwOnError
option. This makes callApi play nice with other libraries that expect a promise to resolve to a value, for example React Query
.
const callMainApi = callApi.create({
throwOnError: true,
});
const { data, error } = useQuery({
queryKey: ["todos"],
queryFn: async () => {
// CallApi will throw an error if the request fails or there is an error response, which react query would handle
const { data } = await callMainApi("todos");
return data;
},
});
Doing this with regular fetch would imply the following extra steps:
const { data, error } = useQuery({
queryKey: ["todos"],
queryFn: async () => {
const response = await fetch("todos");
if (!response.ok) {
throw new Error("Failed to fetch");
}
return response.json();
},
});
For even more convenience, you can specify a resultMode for callApi in addition with the throwOnError option. Use this if you feel to lazy to make a tiny wrapper over callApi for something like react query:
const callMainApi = callApi.create({
throwOnError: true,
resultMode: "onlySuccess",
});
const { data, error } = useQuery({
queryKey: ["todos"],
// CallApi will throw on errors here, and also return only data, which react query is interested in
queryFn: () => callMainApi("todos"),
});
- You can provide types for the success and error data via generics, to enable autocomplete and type checking in your codebase.
const callMainApi = callApi.create<FormResponseDataType, FormErrorResponseType>({
baseURL: BASE_AUTH_URL,
method: "POST",
retries: 3,
credentials: "same-origin",
});
- Just like the fetch options, all type parameters (generics) can also be overriden per instance level
const { data } = callMainApi<NewResponseDataType>({
method: "GET",
retries: 5,
});
- Since the
data
anderror
properties destructured from callApi are in a discriminated union, simply checking for and handling theerror
property will narrow down the type of the data. The reverse case also holds (checking for data to narrow error type).
This simply means that if data is available error will be null, and if error is available data will be null. Both cannot exist at the same time.
// As is, both data and error could be null
const { data, error } = await callMainApi("some-url", {
body: { message: "Good game" },
});
if (error) {
console.error(error);
return;
}
// Now, data is no longer null
console.log(data);
- CallApi provides a type guard that allows differentiate between an HTTPError and standard js errors. It also helps narrow down the discriminated union type of the error object.
import { isHTTPError } from "@zayne-labs/callapi";
const { data, error } = await callMainApi("some-url", {
body: { message: "Good game" },
});
if (isHTTPError(error)) {
console.error(error.name); // `HTTPError`
console.error(error.message); // contains the parsed error message, if the response from the server contains such a property
console.error(error.errorData); // contains the parsed error response
return;
}
if (error) {
console.error(error.name); // contains the name of the error
console.error(error.message); // contains the error message
console.error(error.errorData); // contains the original error object
}
- The types for the object passed to onResponse and onResponseError could be augmented with type helpers provided by
@zayne-labs/callapi
.
const callAnotherApi = callApi.create({
onResponseError: ({ response, request, options }: ResponseErrorContext<{ message?: string }>) => {
// Log error response
console.log(
request,
response.status,
// error data, coming back from api
response.errorData,
// Typescript will then understand the errorData might contains a message property
response.errorData?.message
);
},
});
-
All Regular fetch options are supported as is, with only body extended to support more types.
-
body
: Optional body of the request, can be an object or any other supported body type. -
query
: Query parameters to append to the URL. -
auth
: Authorization header value. -
bodySerializer
: Custom function to serialize the body object into a string. -
responseParser
: Custom function to parse the response string into an object. -
resultMode
: Mode of the result, can influence how results are handled or returned. (default: "all") -
cancelRedundantRequests
: If true, cancels previous unfinished requests to the same URL. (default: true) -
baseURL
: Base URL to be prepended to all request URLs. -
timeout
: Request timeout in milliseconds. -
defaultErrorMessage
: Default error message to use if none is provided from a response. (default: "Failed to fetch data from server!") -
throwOnError
: If true or the function returns true, throws errors instead of returning them. -
responseType
: Expected response type, affects how response is parsed. (default: "json") -
retries
: Number of retry attempts for failed requests. (default: 0) -
retryDelay
: Delay between retries in milliseconds. (default: 500) -
retryCodes
: HTTP status codes that trigger a retry. (default: [409, 425, 429, 500, 502, 503, 504]) -
retryMethods
: HTTP methods that are allowed to retry. (default: ["GET", "POST"]) -
meta
: An optional field that can contain additional information about a request, which could be helpful in differentiating between different requests in a shared interceptor. -
onRequest
: Interceptor called just before the request is made, allowing for modifications or additional operations. -
onRequestError
: Interceptor called when an error occurs during the fetch request. -
onResponse
: Interceptor called when a successful response is received from the API. -
onResponseError
: Interceptor called when an error response is received from the API.
callApi.create(options)
: Creates an instance ofcallApi
with shared base configurations.callApi.cancel(url: string)
: Cancels an ongoing request to the specified URL.
-
isHTTPError
: Type guard for if an error is an HTTPError -
isHTTPErrorInstance
: Type guard for if an error is an instance of HTTPError. Useful for whenthrowAllErrors
option is set totrue
-
toQueryString
: Converts an object to a URL query string
- Credits to
ofetch
by unjs for some of the ideas for the features in the library like the function-based interceptors, retries etc - Credits to
zl-fetch
fetch wrapper as well for the inspiration behind a few features in this library as well