Small wrapper around the native fetch API.
npm i -S @thepassle/app-tools
import { Api } from "@thepassle/app-tools/api.js";
/** Using defaults: */
const api = new Api();
/** Or with configuration: */
const api = new Api({
baseURL: "https://api.foo.com",
responseType: "text",
plugins: [
{
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response,
transform: (data) => data,
handleError: (e) => true,
},
],
});
const user = await api.get("/users/1");
await api.post("/form/submit", {
name: "John Doe",
email: "johndoe@internet.com",
});
try {
await api.get("/foo");
} catch (e) {
console.log(e); // StatusError
e.message; // the `statusText` of the `response`
e.response; // access the `response`
}By default api will throw an error if a response is not ok (!response.ok). If this is the case, it will throw a StatusError. The StatusError is thrown with the response.statusText as message, and also has the actual response available on it: e.response.
Use plugins to customize your requests to fit your needs
import { retry } from "@thepassle/app-tools/api/plugins/retry.js";
/** Retries failed requests up to 5 times with exponential backoff */
api.get(url, { plugins: [retry()] });
/** Or with custom options: */
api.get(url, {
plugins: [
retry({
maxRetries: 3,
delays: [500, 1000, 2000],
shouldRetry: (e) => e.message !== "AbortError",
}),
],
});import {
logger,
loggerPlugin,
} from "@thepassle/app-tools/api/plugins/logger.js";
/** Logs metadata to the console */
api.get(url, { plugins: [logger] });
/** Or */
const logger = loggerPlugin({ collapsed: false });
api.get(url, { plugins: [logger] });import { cache, cachePlugin } from "@thepassle/app-tools/api/plugins/cache.js";
/** Caches the response for a default of 10 minutes */
api.get(url, { plugins: [cache] });
/** Or */
const cache = cachePlugin({ maxAge: 1000 });
api.get(url, { plugins: [cache] });import {
debounce,
debouncePlugin,
} from "@thepassle/app-tools/api/plugins/debounce.js";
/** Debounces the response for a default of 1000 ms */
api.get(url, { plugins: [debounce] });
/** Or */
const debounce = debouncePlugin({ maxAge: 2000 });
api.get(url, { plugins: [debounce] });Note: The debounce plugin wraps the fetchFn in a debouncer. awaiting the call will cause the debounce to be awaited. E.g.:
api.get(url, { plugins: [debounce] }).then(() => {
console.log(1);
});
api.get(url, { plugins: [debounce] }).then(() => {
console.log(2);
});
// Output:
// 2But awaiting it will become:
await api.get(url, { plugins: [debounce] }).then(() => {
console.log(1);
});
await api.get(url, { plugins: [debounce] }).then(() => {
console.log(2);
});
// Output:
// 1
// 2import { abort } from "@thepassle/app-tools/api/plugins/abort.js";
/** Aborts previous, unfinished requests via an AbortController if requests are fired in quick succession, like spammy clicks on buttons */
api.get(url, { plugins: [abort] });import {
concurrency,
concurrencyPlugin,
} from "@thepassle/app-tools/api/plugins/concurrency.js";
/** Limits amount of concurrent requests to 10 by default */
await Promise.all(
bigArray.map((url) => {
return api.get(url, { plugins: [concurrency] });
}),
);
/**
* Or customize the amount of concurrent requests
*/
const concurrency = concurrencyPlugin(20);
await Promise.all(
bigArray.map((url) => {
return api.get(url, { plugins: [concurrency] });
}),
);import { mock } from "@thepassle/app-tools/api/plugins/mock.js";
import { delay, delayPlugin } from "@thepassle/app-tools/api/plugins/delay.js";
/** Easily mock requests during development using the native `Response` object */
api.get(url, {
plugins: [
mock(() => new Response(JSON.stringify({ foo: "bar" }))),
delay, // defaults to 1000ms
],
});
/** Or */
const delay = delayPlugin(2000);
api.get(url, { plugins: [delay] });import {
jsonPrefix,
jsonPrefixPlugin,
} from "@thepassle/app-tools/api/plugins/jsonPrefix.js";
/** Add plugins to run on all requests */
const api = new Api({ plugins: [jsonPrefix] });
/** Or */
const jsonPrefix = jsonPrefixPlugin("<prefix>");
const api = new Api({ plugins: [jsonPRefix] });import { xsrf, xsrfPlugin } from "@thepassle/app-tools/api/plugins/xsrf.js";
/** Add plugins to run on all requests */
const api = new Api({ plugins: [xsrf] });
/** Or */
const xsrf = xsrfPlugin({
xsrfCookieName: "",
xsrfHeaderName: "",
});
const api = new Api({ plugins: [xsrf] });import { logger } from "@thepassle/app-tools/api/plugins/logger.js";
/** Add plugins to run on all requests */
const api = new Api({ plugins: [logoutOnUnauthorized, logger] });api.get(url, opts);
api.options(url, opts);
api.delete(url, opts);
api.head(url, opts);
api.post(url, data, opts);
api.put(url, data, opts);
api.patch(url, data, opts);
api.addPlugin({
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response,
transform: (data) => data,
handleError: (e) => true,
});api.get(url, {
baseURL: "https://api.foo.com",
responseType: "text",
params: { foo: "bar" },
plugins: [
{
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response,
transform: (data) => data,
handleError: (e) => true,
},
],
// Also supports all the options of the native fetch API
// mode, credentials, cache, redirect, referrer, integrity, keepalive, signal, referrerPolicy, headers, method
});BaseURL to resolve all requests from. Can be set globally when instantiating a new Api instance, or on a per request basis. When set on a per request basis, will override the globally set baseURL (if set)
api.get(url, { baseURL: "https://api.foo.com" });Overwrite the default responseType ('json')
api.get(url, { responseType: "text" });An object to be queryParam-ified and added to the request url
api.get(url, { params: { foo: "bar" } });An array of plugins.
api.get(url, {
plugins: [
{
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response,
transform: (data) => data,
handleError: (e) => true,
},
],
});You can also use plugins. You can add plugins on a per-request basis, or you can globally add them to your api instance:
const api = new Api({
plugins: [
{
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response,
transform: (data) => data,
handleError: (e) => true,
},
],
});You can also dynamically add plugins:
api.addPlugin({
afterFetch: ({ response }) => response,
});Or you can add them on a per request basis:
api.get(url, {
plugins: [
{
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response,
transform: (data) => data,
handleError: (e) => true,
},
],
});Run logic before the actual fetch call happens, or alter/modify the meta information of a request.
If you want to alter or modify the meta information of a request, make sure to return the value.
api.get("/foo", {
plugins: [
{
beforeFetch: (meta) => ({ ...meta, url: "/bar" }),
},
],
});
// RESULT: url `/bar` gets called instead of `/foo`If you dont want to alter or modify any meta information of the request, you dont have to return anything.
{
beforeFetch: ({ url }) => {
console.log(url);
};
}Runs immediately after the fetch call happened. afterFetch should always return a Response:
{
afterFetch: ({
response,
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => response;
}
{
afterFetch: ({ response }) =>
new Response(JSON.stringify({ foo: "bar" }), response);
}Runs after the Response object has been handled according to the responseType, (e.g.: res.json()). Can be used to transform the returned data:
Should always return the data.
{
transform: (data) => data;
}Whether or not an error should throw. Return true if an error should throw, return false if an error should be ignored.
{
handleError: (e) => e.message !== "AbortError";
}function requestLogger() {
let start;
return {
beforeFetch: () => {
start = Date.now();
},
afterFetch: () => {
console.log(`Request took ${Date.now() - start}ms`);
},
};
}
api.addPlugin(requestLogger());api.get(url, {
plugins: [
{
beforeFetch: ({
url,
headers,
fetchFn,
responseType,
baseURL,
method,
opts,
data,
}) => {},
afterFetch: ({ response }) => {
if (response.status === 401 || response.status === 403) {
logout();
}
return response;
},
},
],
});If you want to access the response body of your response in a plugin, make sure to clone the response:
const myPlugin = {
afterFetch: async ({ response: originalResponse }) => {
const clone = originalResponse.clone();
let data = await clone.text(); // or `.json()` etc
data = data.replaceAll("foo", "bar");
// Always make sure to return a `Response`
return new Response(data, originalResponse);
},
};
api.addPlugin(myPlugin);You can also overwrite the response entirely by returning a new Response
api.addPlugin({
afterFetch: async ({response}) => new Response(JSON.stringify({foo: 'bar'}), response);
});You can also overwrite the fetch implementation to use:
api.addPlugin({
beforeFetch: (meta) => ({
...meta,
fetchFn: () => Promise.resolve(new Response("{}")),
}),
});Do note that if you use multiple plugins that overwrite the fetchFn, the last plugin to overwrite the fetchFn will win, there can only be one fetchFn.
api.addPlugin({
// Adds a `.foo` property to all of your response data
transform: (data) => {
data.foo = "bar";
return data;
},
});