simple-api-utils
v1.0.0
Published
- Simpler API - Method shortcuts - Treats non-2xx status codes as errors (after redirects) - Retries failed requests - JSON option - Timeout support - URL prefix option - Instances with custom defaults - Hooks
Downloads
6
Maintainers
Readme
Benefits over plain fetch
- Simpler API
- Method shortcuts
- Treats non-2xx status codes as errors (after redirects)
- Retries failed requests
- JSON option
- Timeout support
- URL prefix option
- Instances with custom defaults
- Hooks
Install
npm install
Usage
import apiUtil from 'apiUtil';
const json = await apiUtil.post('https://example.com', {json: {foo: true}}).json();
console.log(json);
//=> `{data: '🦄'}`
With plain fetch
, it would be:
class HTTPError extends Error {}
const response = await fetch('https://example.com', {
method: 'POST',
body: JSON.stringify({foo: true}),
headers: {
'content-type': 'application/json'
}
});
if (!response.ok) {
throw new HTTPError(`Fetch error: ${response.statusText}`);
}
const json = await response.json();
console.log(json);
//=> `{data: '🦄'}`
If you are using Deno, import apiUtil from a URL. For example, using a CDN:
import apiUtil from 'https://esm.sh/apiUtil';
API
apiUtil(input, options?)
apiUtil.get(input, options?)
apiUtil.post(input, options?)
apiUtil.put(input, options?)
apiUtil.patch(input, options?)
apiUtil.head(input, options?)
apiUtil.delete(input, options?)
Sets options.method
to the method name and makes a request.
When using a Request
instance as input
, any URL altering options (such as prefixUrl
) will be ignored.
options
Type: object
In addition to all the fetch
options, it supports these options:
method
Type: string
Default: 'get'
HTTP method used to make the request.
Internally, the standard methods (GET
, POST
, PUT
, PATCH
, HEAD
and DELETE
) are uppercased in order to avoid server errors due to case sensitivity.
json
searchParams
Type: string | object<string, string | number | boolean> | Array<Array<string | number | boolean>> | URLSearchParams
Default: ''
prefixUrl
Type: string | URL
import apiUtil from 'apiUtil';
// On https://example.com
const response = await apiUtil('unicorn', {prefixUrl: '/api'});
//=> 'https://example.com/api/unicorn'
const response2 = await apiUtil('unicorn', {prefixUrl: 'https://cats.com'});
//=> 'https://cats.com/unicorn'
retry
Type: object | number
Default:
limit
:2
methods
:get
put
head
delete
options
trace
statusCodes
:408
413
429
500
502
503
504
maxRetryAfter
:undefined
backoffLimit
:undefined
delay
:attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000
An object representing limit
, methods
, statusCodes
and maxRetryAfter
fields for maximum retry count, allowed methods, allowed status codes and maximum Retry-After
time.
If retry
is a number, it will be used as limit
and other defaults will remain in place.
If maxRetryAfter
is set to undefined
, it will use options.timeout
. If Retry-After
header is greater than maxRetryAfter
, it will cancel the request.
The backoffLimit
option is the upper limit of the delay per retry in milliseconds.
To clamp the delay, set backoffLimit
to 1000, for example.
By default, the delay is calculated with 0.3 * (2 ** (attemptCount - 1)) * 1000
. The delay increases exponentially.
The delay
option can be used to change how the delay between retries is calculated. The function receives one parameter, the attempt count, starting at 1
.
Retries are not triggered following a timeout.
import apiUtil from 'apiUtil';
const json = await apiUtil('https://example.com', {
retry: {
limit: 10,
methods: ['get'],
statusCodes: [413],
backoffLimit: 3000
}
}).json();
timeout
Type: number | false
Default: 10000
Timeout in milliseconds for getting a response, including any retries. Can not be greater than 2147483647.
If set to false
, there will be no timeout.
hooks
Type: object<string, Function[]>
Default: {beforeRequest: [], beforeRetry: [], afterResponse: []}
Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially.
hooks.beforeRequest
Type: Function[]
Default: []
import apiUtil from 'apiUtil';
const api = apiUtil.extend({
hooks: {
beforeRequest: [
request => {
request.headers.set('X-Requested-With', 'apiUtil');
}
]
}
});
const response = await api.get('https://example.com/api/users');
hooks.beforeRetry
Type: Function[]
Default: []
import apiUtil from 'apiUtil';
const response = await apiUtil('https://example.com', {
hooks: {
beforeRetry: [
async ({request, options, error, retryCount}) => {
const token = await apiUtil('https://example.com/refresh-token');
request.headers.set('Authorization', `token ${token}`);
}
]
}
});
hooks.beforeError
Type: Function[]
Default: []
This hook enables you to modify the HTTPError
right before it is thrown. The hook function receives a HTTPError
as an argument and should return an instance of HTTPError
.
import apiUtil from 'apiUtil';
await apiUtil('https://example.com', {
hooks: {
beforeError: [
error => {
const {response} = error;
if (response && response.body) {
error.name = 'GitHubError';
error.message = `${response.body.message} (${response.status})`;
}
return error;
}
]
}
});
hooks.afterResponse
Type: Function[]
Default: []
import apiUtil from 'apiUtil';
const response = await apiUtil('https://example.com', {
hooks: {
afterResponse: [
(_request, _options, response) => {
// You could do something with the response, for example, logging.
log(response);
// Or return a `Response` instance to overwrite the response.
return new Response('A different response', {status: 200});
},
// Or retry with a fresh token on a 403 error
async (request, options, response) => {
if (response.status === 403) {
// Get a fresh token
const token = await apiUtil('https://example.com/token').text();
// Retry with the token
request.headers.set('Authorization', `token ${token}`);
return apiUtil(request);
}
}
]
}
});
throwHttpErrors
Type: boolean
Default: true
onDownloadProgress
Type: Function
Download progress event handler.
import apiUtil from 'apiUtil';
const response = await apiUtil('https://example.com', {
onDownloadProgress: (progress, chunk) => {
// Example output:
// `0% - 0 of 1271 bytes`
// `100% - 1271 of 1271 bytes`
console.log(`${progress.percent * 100}% - ${progress.transferredBytes} of ${progress.totalBytes} bytes`);
}
});
parseJson
Type: Function
Default: JSON.parse()
import apiUtil from 'apiUtil';
import bourne from '@hapijs/bourne';
const json = await apiUtil('https://example.com', {
parseJson: text => bourne(text)
}).json();
stringifyJson
Type: Function
Default: JSON.stringify()
User-defined JSON-stringifying function.
Use-cases:
- Stringify JSON with a custom
replacer
function.
import apiUtil from 'apiUtil';
import {DateTime} from 'luxon';
const json = await apiUtil('https://example.com', {
stringifyJson: data => JSON.stringify(data, (key, value) => {
if (key.endsWith('_at')) {
return DateTime.fromISO(value).toSeconds();
}
return value;
})
}).json();
fetch
Type: Function
Default: fetch
import apiUtil from 'apiUtil';
import fetch from 'isomorphic-unfetch';
const json = await apiUtil('https://example.com', {fetch}).json();
apiUtil.extend(defaultOptions)
import apiUtil from 'apiUtil';
const url = 'https://sindresorhus.com';
const original = apiUtil.create({
headers: {
rainbow: 'rainbow',
unicorn: 'unicorn'
}
});
const extended = original.extend({
headers: {
rainbow: undefined
}
});
const response = await extended(url).json();
console.log('rainbow' in response);
//=> false
console.log('unicorn' in response);
//=> true
apiUtil.create(defaultOptions)
Create a new apiUtil instance with complete new defaults.
import apiUtil from 'apiUtil';
// On https://my-site.com
const api = apiUtil.create({prefixUrl: 'https://example.com/api'});
const response = await api.get('users/123');
//=> 'https://example.com/api/users/123'
const response = await api.get('/status', {prefixUrl: ''});
//=> 'https://my-site.com/status'
defaultOptions
Type: object
apiUtil.stop
import apiUtil from 'apiUtil';
const options = {
hooks: {
beforeRetry: [
async ({request, options, error, retryCount}) => {
const shouldStopRetry = await apiUtil('https://example.com/api');
if (shouldStopRetry) {
return apiUtil.stop;
}
}
]
}
};
// Note that response will be `undefined` in case `apiUtil.stop` is returned.
const response = await apiUtil.post('https://example.com', options);
// Using `.text()` or other body methods is not supported.
const text = await apiUtil('https://example.com', options).text();
HTTPError
try {
await apiUtil('https://example.com').json();
} catch (error) {
if (error.name === 'HTTPError') {
const errorJson = await error.response.json();
}
}
TimeoutError
Tips
Sending form data
import apiUtil from 'apiUtil';
// `multipart/form-data`
const formData = new FormData();
formData.append('food', 'fries');
formData.append('drink', 'icetea');
const response = await apiUtil.post(url, {body: formData});
import apiUtil from 'apiUtil';
// `application/x-www-form-urlencoded`
const searchParams = new URLSearchParams();
searchParams.set('food', 'fries');
searchParams.set('drink', 'icetea');
const response = await apiUtil.post(url, {body: searchParams});
Setting a custom Content-Type
import apiUtil from 'apiUtil';
const json = await apiUtil.post('https://example.com', {
headers: {
'content-type': 'application/json'
},
json: {
foo: true
},
}).json();
console.log(json);
//=> `{data: '🦄'}`
Cancellation
Example:
import apiUtil from 'apiUtil';
const controller = new AbortController();
const {signal} = controller;
setTimeout(() => {
controller.abort();
}, 5000);
try {
console.log(await apiUtil(url, {signal}).text());
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
}