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: (res) => res,
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 { 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. await
ing 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:
// 2
But 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
// 2
import { 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 { 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: (res) => res,
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: (res) => res,
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: (res) => res,
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: (res) => res,
transform: (data) => data,
handleError: (e) => true
}
]
});
You can also dynamically add plugins:
api.addPlugin({
afterFetch: (res) => res
});
Or you can add them on a per request basis:
api.get(url, {
plugins: [
{
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
afterFetch: (res) => res,
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: (res) => res; }
{ afterFetch: (res) => new Response(JSON.stringify({foo: 'bar'}), res); }
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: (res) => {
if(res.status === 401 || res.status === 403) {
logout();
}
return res;
}
}
]
});
If you want to access the response body of your response in a plugin, make sure to clone the response:
const myPlugin = {
afterFetch: async (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 (res) => new Response(JSON.stringify({foo: 'bar'}), res);
});
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;
}
})