A lite http request lib based on fetch with plugins support and similar API to axios.
npm install xior



!NPM Weekly Downloads
!NPM Month Downloads
!typescript
A lite http request lib based on fetch with plugin support and similar API to axios.
Features:
- π₯ Use fetch
- π«‘ Similar axios API: axios.create / axios.interceptors / .get/post/put/patch/delete/head/options
- π€ Supports timeout, canceling requests, and nested query encoding
- π₯· Supports plugins: error retry, deduplication, throttling, cache, error cache, mock, and custom plugins
- π Lightweight (~6KB, Gzip ~3kb)
- π Unit tested and strongly typed πͺ
- Intro
- Table of Contents
- Getting Started
- Installing
- Package manager
- Use CDN
- Create instance
- GET / POST / DELETE / PUT / PATCH / OPTIONS / HEAD
- Change default headers or params
- Get response headers
- Upload file
- Using interceptors
- Cleanup interceptors
- Timeout and Cancel request
- Proxy or use custom fetch implementations
- Custom data parser
- Encrypt and Decrypt Example
- Tips: Make your SSR(Server-side Rendering) app more stable and faster
- Plugins
- Error retry plugin
- Request throttle plugin
- Request dedupe plugin
- Error cache plugin
- Cache plugin
- Persist cache data
- Upload and download progress plugin
- Mock plugin
- Auth refresh token plugin(from community)
- Auth refresh token plugin(built-in)
- Create your own custom plugin
- Cleanup plugins example
- Helper functions
- FAQ
- 1. Is xior 100% compatible with axios?
- 2. Can I use xior in projects like Bun, Expo, React Native, RemixJS, Next.js, Vue, Nuxt.js, Tauri or NervJS/Taro?
- 3. How can I use custom fetch implementation or How to support proxy feature?
- 4. How do I handle responses with types like 'stream', 'document', 'arraybuffer', or 'blob'?
- 5. How do I change default throw error behaviour, validStatus or validResponse?
- 6. How do I support older browsers?
- 7. Why is xior named "xior"?
- 8. Where can I ask additional questions?
- Migrate from axios to xior
- GET
- POST
- axios(requestObj): axios({ method: 'get', params: { a: 1 } })
- Creating an instance
- Get response headers
- transformRequest
- transformResponse
- Download file with responseType: 'stream' | 'blob'
- Use stream
- Migrate from fetch to xior
- GET
- POST
- Abort a fetch
- Sending a request with credentials included
- Uploading a file
- Processing a text file line by line
- API Reference
- Star History
- Thanks
#### Package manager
``shnpm
npm install xior
#### Use CDN
> Since v0.2.1, xior supports UMD format
Use jsDelivr CDN:
`html
`Use unpkg CDN:
`html
`$3
`ts
import xior from 'xior';export const xiorInstance = xior.create({
baseURL: 'https://apiexampledomain.com/api',
headers: {
// put your common custom headers here
},
});
`$3
GET
>
HEAD / DELETE / OPTIONS are same usage with GET method`ts
async function run() {
const { data } = await xiorInstance.get('/'); // with params and support nested params
const { data: data2 } = await xiorInstance.get('/', { params: { a: 1, b: 2, c: { d: 1 } } });
// with headers
const { data: data3 } = await xiorInstance.get('/', {
params: { a: 1, b: 2 },
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
// types
const { data: data4 } = await xiorInstance.get<{ field1: string; field2: number }>('/');
}
`POST
>
PUT/PATCH methods are same usage with POST`ts
async function run() {
const { data: data3 } = await xiorInstance.post<{ field1: string; field2: number }>(
'/',
{ a: 1, b: '2' },
{
params: { id: 1 },
headers: {},
}
);
}
`$3
`ts
import xior from 'xior';export const xiorInstance = xior.create({
baseURL: 'https://apiexampledomain.com/api',
});
function setAccessToken(token: string) {
// xiorInstance.defaults.params['x'] = 1;
xiorInstance.defaults.headers['Authorization'] =
Bearer ${token};
}function removeUserToken() {
// delete xiorInstance.defaults.params['x'];
delete xiorInstance.defaults.headers['Authorization'];
}
`$3
`ts
import xior from 'xior';const xiorInstance = xior.create({
baseURL: 'https://apiexampledomain.com/api',
});
const { data, headers } = await xiorInstance.get('/');
console.log(headers.get('X-Header-Name'));
`$3
xior supports file uploads using the
FormData API and provides an optional 'xior/plugins/progress' plugin for simulating upload progress, usage similar to Axios.`ts
import Xior from 'xior';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';const http = Xior.create({});
http.plugins.use(
uploadDownloadProgressPlugin({
progressDuration: 5 * 1000,
})
);
const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');
http.post('/upload', formData, {
onUploadProgress(e) {
console.log(
Upload progress: ${e.progress}%);
},
// progressDuration: 10 * 1000
});
`$3
xior supports interceptors similar to Axios, allowing you to modify requests and handle responses programmatically.
Request interceptors:
`ts
import xior, { merge } from 'xior';const http = xior.create({
// ...options
});
http.interceptors.request.use((config) => {
const token = localStorage.getItem('REQUEST_TOKEN');
if (!token) return config;
return merge(config, {
headers: {
Authorization:
Bearer ${token},
},
});
});// One more interceptors for request
http.interceptors.request.use((config) => {
return config;
});
async function getData() {
const { data } = await http.get('/');
console.log(data);
return data;
}
`Response interceptors:
`ts
import xior, { merge } from 'xior';const http = xior.create({});
http.interceptors.response.use(
(result) => {
const { data, request: config, response: originalResponse } = result;
return result;
},
async (error) => {
if (error instanceof TypeError) {
console.log(
Request error:, error);
}
if (error?.response?.status === 401) {
localStorage.removeItem('REQUEST_TOKEN');
}
return Promise.reject(error);
}
);async function getData() {
const { data } = await http.get('/');
console.log(data);
return data;
}
`$3
`ts
import xior from 'xior';const http = xior.create({});
// Cleanup request interceptors
const handler1 = http.interceptors.request.use((config) => {
return config;
});
http.interceptors.request.eject(handler1);
// Cleanup all request interceptors
// http.interceptors.request.clear()
// Cleanup response interceptors
const handler2 = http.interceptors.response.use((res) => {
return res;
});
http.interceptors.response.eject(handler2);
// Cleanup all response interceptors
// http.interceptors.response.clear()
`$3
Timeout:
`ts
import xior from 'xior';const instance = xior.create({
timeout: 120 * 1000, // set default timeout
});
await instance.post(
'http://httpbin.org',
{
a: 1,
b: 2,
},
{
timeout: 60 1000, // override default timeout 120 1000
}
);
`Cancel request:
`ts
import xior from 'xior';
const instance = xior.create();const controller = new AbortController();
xiorInstance.get('http://httpbin.org', { signal: controller.signal }).then((res) => {
console.log(res.data);
});
class CancelRequestError extends Error {}
controller.abort(new CancelRequestError()); // abort request with custom error
`$3
See 3. How can I use custom fetch implementation or How to support proxy feature?
$3
In
xior, the default response parser is this:`ts
let data = response.text();
if (data) {
try {
data = JSON.parse(data);
} catch (e) {}
}
return data;
`But maybe we don't want to do it this way; instead, we want to parse the data based on the
content-type from response's headers. So, we can do it this way:`ts
import axios from 'xior';const http = Xior.create({
baseURL,
responseType: 'custom', // Tell xior no need to parse body
});
// Define content type matchers as [responseMethod, [regexpPatterns]]
const typeMatchers = [
['json', [/^application\/.*json$/, /^$/]],
['text', [/^text\//, /^image\/svg\+xml$/, /^application\/.*xml$/]],
// ['arrayBuffer', [/^application\/octet-stream/]],
] as const;
http.interceptors.response.use(
async (res) => {
try {
if (res.config.responseType !== 'custom') return res;
const { response } = res;
const headers = response?.headers;
if (!response || headers.get('Content-Length') === '0') return res;
const contentType = headers.get('Content-Type')?.split(';')?.[0]?.trim() || '';
// Find matching response method using the typeMatchers array
const matchedType = typeMatchers.find(([_, patterns]) =>
patterns.some((pattern) => pattern.test(contentType))
);
if (matchedType) {
const [method] = matchedType;
res.data = await response[method]();
} else {
console.warn(
Unknown Content-Type: ${contentType});
} return res;
} catch (error) {
console.error('Interceptor error:', error);
return Promise.reject(error);
}
},
(error) => Promise.reject(error)
);
`$3
We can use interceptors easily to handle encrypt/decrypt.
Create
encryption.ts:`ts
// encryption.ts
export const SECRET = '&&^SDxsdasdas776';export function encrypt(data: string) {
return data + '____' + SECRET;
}
export function decrypt(data: string, s?: string) {
return data.replace('____' + (s || SECRET), '');
}
`Create
xior-instance.ts:`ts
import xior from 'xior';import { SECRET, encrypt, decrypt } from './encryption';
export const instance = xior.create();
instance.interceptors.request.use((req) => {
req.headers['X'] = SECRET;
if (req.url && req.data) {
const result = JSON.stringify(req.data);
const blob = encrypt(result);
req.data = { blob };
}
return req;
});
instance.interceptors.response.use((res) => {
if (res.request.url && res.data?.blob) {
res.data = decrypt(res.data.blob);
try {
res.data = JSON.parse(res.data);
} catch (e) {
console.error(e);
}
}
return res;
});
`> Check test code in
tests/src/tests/encrypt-decrypt/$3
How do we achieve this? By using Xior's plugins:
1. If a
GET request fails, allow retries for a second chance at success.
2. If retries still fail, return cached data (if available) to prevent page crashes or error pages.
3. Deduplicate GET requests to avoid redundant calls.
4. Throttle GET requests to control request frequency.
5. For large data that isnβt needed in real-time (like i18n JSON files), serve cached data first and fetch updates in the background.
Example code:`ts
import xior, { XiorError as AxiosError } from 'xior';
import retryPlugin from 'xior/plugins/error-retry';
import dedupePlugin from 'xior/plugins/dedupe';
import throttlePlugin from 'xior/plugins/throttle';
import cachePlugin from 'xior/plugins/cache';
import errorCachePlugin from 'xior/plugins/error-cache';// Setup
const http = axios.create({
baseURL: 'http://localhost:3000',
});
http.plugins.use(throttlePlugin()); // Throttle same
GET request in 1000ms
http.plugins.use(dedupePlugin()); // Prevent same GET requests from occurring simultaneously.
http.plugins.use(retryPlugin());
http.plugins.use(cachePlugin());
http.plugins.use(errorCachePlugin());// 1. If
GET data error, at least have chance to retry;
// 2. If retry still error, return the cache data(if have) to prevent page crash or show error page;
const res = await http.get('/api/get-data'); // these will retry if have error
if (res.fromCache) {
console.log(the data from cahce, res.cacheTime);
}// 3. Dedupe the same
GET requests, this will only sent 1 real request
await Promise.all([
http.get('/api/get-data-2'),
http.get('/api/get-data-2'),
http.get('/api/get-data-2'),
]);// 4. Throttle the
GET requests,
// we want throttle some larget data request in 10s, default is 1s
http.get('/api/get-some-big-data', { threshold: 10e3 });// 5. If have cache data, return the cache data first,
// and run the real request in background
http.get('/api/get-some-big-data', { threshold: 10e3, useCacheFirst: true });
`> Above plugins execution order: errorCache β cache β retry β dedupe β throttle β real request
Plugins
xior offers a variety of built-in plugins to enhance its functionality:
> The plugin mechanism is: first in, last run.
For example:
`ts
plugins.use(plugin1);
plugins.use(plugin2);
plugins.use(plugin3);
`> Run order: plugin3 β plugin2 β plugin1
- Error retry plugin
- Request dedupe plugin
- Request throttle plugin
- Error cache plugin
- Cache plugin
- Upload and download progress plugin
- Mock plugin
- Auth refresh token plugin(from community)
- Auth refresh token plugin(built-in)
Usage:
`ts
import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
import throttlePlugin from 'xior/plugins/throttle';
import cachePlugin from 'xior/plugins/cache';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';const http = xior.create();
http.plugins.use(errorRetryPlugin());
http.plugins.use(throttlePlugin());
http.plugins.use(cachePlugin());
http.plugins.use(uploadDownloadProgressPlugin());
`$3
> Retry the failed request with special times
API:
`ts
function errorRetryPlugin(options: {
retryTimes?: number;
retryInterval?: number | ((errorCount: number) => number);
enableRetry?: boolean | (config: XiorRequestConfig, error: XiorError | Error) => boolean | undefined;
onRetry?: (config: XiorRequestConfig, error: XiorError | Error, count: number) => void;
}): XiorPlugin;
`The
options object:| Param | Type | Default value | Description |
| ------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| retryTimes | number | 2 | Set the retry times for failed request |
| retryInterval | number \| ((errorCount: number, config: XiorRequestConfig, error: XiorError) => number) | 3000 | After first time retry, the next retries interval time, default interval is 3 seconds; you can use function as param to return interval number too |
| enableRetry | boolean \| ((config: XiorRequestConfig, error: XiorError \| Error) => boolean \| undefined) | (config, error) => config.method === 'GET' \|\| config.isGet | Default only retry if
GET request error and retryTimes > 0 |
| onRetry | boolean \| ((config: XiorRequestConfig, error: XiorError \| Error, count: number) => void) | undefined | For log retry info |Basic usage:
`ts
import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';const http = xior.create();
http.plugins.use(
errorRetryPlugin({
retryTimes: 3,
// retryInterval: 3000,
retryInterval(count, config, error) {
// if (error.response?.status === 500) return 10e3;
return count * 1e3;
},
onRetry(config, error, count) {
console.log(
${config.method} ${config.url} retry ${count} times);
},
// enableRetry(config, error) {
// if ([401, 400].includes(error.response?.status)) { // no retry when status is 400 or 401
// return false;
// }
// // no return or return undefined here, will reuse the default enableRetry logic
// },
})
);// if request error, max retry 3 times until success
http.get('/api1');
// if request error, will not retry, because
retryTimes: 0
http.get('/api2', { retryTimes: 0 });// if POST request error, will not retry
http.post('/api1');
// Use
enableRetry: true to support post method, max retry 5 times until success
http.post('/api1', null, { retryTimes: 5, enableRetry: true });
`Advance usage:
> The retry key for the unique request generated by use
params and data, if your request depends on headers, you can add request interceptor to add headers's value to params:`ts
import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';const http = xior.create();
http.plugins.use(errorRetryPlugin());
http.interceptors.request.use((config) => {
config.params['___k'] =
${config.headers['x-custom-field'] || ''};
return config;
});
`Use CDN:
Using jsDelivr CDN:
`html
`Using unpkg CDN:
`html
`$3
> Throttle GET requests(or custom) most once per threshold milliseconds, filter repeat requests in certain time.
API:
`ts
function throttleRequestPlugin(options: {
/* threshold in milliseconds, default: 1000ms /
threshold?: number;
/**
* check if we need enable throttle, default only GET method orisGet: true enable
*/
enableThrottle?: boolean | ((config?: XiorRequestConfig) => boolean | undefined);
throttleCache?: ICacheLike;
onThrottle?: (config: XiorRequestConfig) => void;
throttleItems?: number;
}): XiorPlugin;
`The
options object:> You can override default value in each request's own config (Except
throttleCache)| Param | Type | Default value | Description |
| -------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| threshold | number | 1000 | The number of milliseconds to throttle request invocations to |
| enableThrottle | boolean \| ((config: XiorRequestConfig) => boolean \| undefined) | (config) => config.method === 'GET' \|\| config.isGet | Default only enabled in
GET request |
| throttleCache | CacheLike | lru(100) | CacheLike instance that will be used for storing throttled requests, use tiny-lru module |
| throttleItems | number | 100 | The max number of throttle items in the default LRU cache |Basic usage:
`ts
import xior from 'xior';
import throttlePlugin from 'xior/plugins/throttle';const http = xior.create();
http.plugins.use(
throttlePlugin({
onThrottle(config) {
console.log(
Throttle requests ${config.method} ${config.url});
},
})
);http.get('/'); // make real http request
http.get('/'); // response from cache
http.get('/'); // response from cache
http.get('/', { throttle: 2e3 }); // custom throttle to 2 seconds
http.post('/'); // make real http request
http.post('/'); // make real http request
http.post('/'); // make real http request
http.post('/', null, {
enableThrottle: true,
}); // make real http request
http.post('/', null, {
enableThrottle: true,
}); // response from cache
http.post('/', null, {
enableThrottle: true,
}); // response from cache
// make post method as get method use
{isGet: true},
// useful when some API is get data but the method is post
http.post('/get', null, {
isGet: true,
}); // make real http request
http.post('/get', null, {
isGet: true,
}); // response from cache
http.post('/get', null, {
isGet: true,
}); // response from cache
`Use CDN:
Using jsDelivr CDN:
`html
`Using unpkg CDN:
`html
`$3
> Prevents having multiple identical requests on the fly at the same time.
API:
`ts
function dedupeRequestPlugin(options: {
/**
* check if we need enable dedupe, default only GET method orisGet: true enable
*/
enableDedupe?: boolean | ((config?: XiorRequestConfig) => boolean);
onDedupe?: (config: XiorRequestConfig) => void;
}): XiorPlugin;
`Basic usage:
`ts
import xior from 'xior';
import dedupePlugin from 'xior/plugins/dedupe';const http = xior.create();
http.plugins.use(
dedupePlugin({
onDedupe(config) {
console.log(
Dedupe ${config.method} ${config.url});
},
})
);http.get('/'); // make real http request
http.get('/'); // response from previous if previous request return response
http.get('/'); // response from previous if previous request return response
http.post('/'); // make real http request
http.post('/'); // make real http request
http.post('/'); // make real http request
`Use CDN:
Using jsDelivr CDN:
`html
`Using unpkg CDN:
`html
`$3
> When request error, if have cached data then use the cached data
API:
`ts
function errorCachePlugin(options: {
enableCache?: boolean | ((config?: XiorRequestConfig) => boolean | undefined);
defaultCache?: ICacheLike;
useCacheFirst?: boolean;
}): XiorPlugin;
`The
options object:| Param | Type | Default value | Description |
| ------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| enableCache | boolean \| ((config: XiorRequestConfig) => boolean \| undefined) | (config) => config.method === 'GET' \|\| config.isGet | Default only enabled in
GET request |
| defaultCache | CacheLike | lru(100, 0) | will used for storing requests by default, except you define a custom Cache with your request config, use tiny-lru module |
| useCacheFirst | boolean | false | If useCacheFirst: true and there's a cache, it will return the cached response first, then run fetching task on the background. This is useful when the response takes a long time, and the data is unnecessary in real-time. |
| cacheItems | number | 100 | The max number of error cache items in the default LRU cache |Basic usage:
`ts
import xior from 'xior';
import errorCachePlugin from 'xior/plugins/error-cache';const http = xior.create();
http.plugins.use(errorCachePlugin({}));
http.get('/users'); // make real http request, and cache the response
const res = await http.get('/users'); // if request error, use the cache data
if (res.fromCache) {
// if
fromCache is true, means data from cache!
console.log('data from cache!');
console.log('data cache timestamp: ', res.cacheTime);
// and get what's the error
console.log('error', res.error);
}http.post('/users'); // no cache for post
http.post('/users', { isGet: true }); // but with
isGet: true can let plugins know this is GET behavior! then will cache data
`Use CDN:
Using jsDelivr CDN:
`html
`Using unpkg CDN:
`html
`$3
> Makes xior cacheable
> Good to Know: Next.js already support cache for fetch in server side. More detail
> Different with
error-cache plugin: this plugin will use the data in cache if the cache data not expired.API:
`ts
function cachePlugin(options: {
enableCache?: boolean | ((config?: XiorRequestConfig) => boolean);
defaultCache?: ICacheLike;
cacheItems?: number;
}): XiorPlugin;
`The
options object:| Param | Type | Default value | Description |
| ------------ | ---------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| enableCache | boolean \| ((config: XiorRequestConfig) => boolean \| undefined) | (config) => config.method === 'GET' \|\| config.isGet | Default only enabled in
GET request |
| defaultCache | CacheLike | lru(100, 1000\60\5) | will used for storing requests by default, except you define a custom Cache with your request config, use tiny-lru module |
| cacheItems | number | 100 | Custom the default LRU cache numbers |
| cacheTime | number | 1000 \ 60 \ 5 | Custom the default LRU cache time |Basic usage:
`ts
import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';const http = xior.create();
http.plugins.use(
cachePlugin({
cacheItems: 100,
cacheTime: 1e3 60 5,
})
);
http.get('/users'); // make real http request
http.get('/users'); // get cache from previous request
http.get('/users', { enableCache: false }); // disable cache manually and the real http request
http.post('/users'); // default no cache for post
// enable cache manually in post request
http.post('/users', { enableCache: true }); // make real http request
const res = await http.post('/users', { enableCache: true }); // get cache from previous request
if (res.fromCache) {
// if
fromCache is true, means data from cache!
console.log('data from cache!', res.cacheKey, res.cacheTime);
}
`Advanced:
`ts
import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';
import { lru } from 'tiny-lru';const http = xior.create({
baseURL: 'https://example-domain.com/api',
headers: { 'Cache-Control': 'no-cache' },
});
http.plugins.use(
cachePlugin({
// disable the default cache
enableCache: false,
cacheItems: 1000,
cacheTime: 1e3 60 10,
})
);
http.get('/users', { enableCache: true }); // manually enable cache for this request
http.get('/users', { enableCache: true }); // get cache from previous request
const cacheA = lru(100);
// a actual request made and cached due to force update configured
http.get('/users', { enableCache: true, defaultCache: cacheA, forceUpdate: true });
`$3
How to persist cache data to the filesystem to prevent loss after a server restart?
For more details, refer to this GitHub issue: GitHub issue 33
$3
> Enable upload and download progress like axios, but the progress is simulated,
> This means it doesn't represent the actual progress but offers a user experience similar to libraries like axios.
API:
`ts
function progressPlugin(options: {
/* default: 51000 ms */
progressDuration?: number;
}): XiorPlugin;
`The
options object:| Param | Type | Default value | Description |
| ---------------- | ------ | ------------- | ---------------------------------------------------- |
| progressDuration | number | 5000 | The upload or download progress grow to 99% duration |
Basic usage:
`ts
import xior from 'xior';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';const http = xior.create({});
http.plugins.use(uploadDownloadProgressPlugin());
const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');
http.post('/upload', formData, {
// simulate upload progress to 99% in 10 seconds, default is 5 seconds
progressDuration: 10 * 1000,
onUploadProgress(e) {
console.log(
Upload progress: ${e.progress}%);
},
// onDownloadProgress(e) {
// console.log(Download progress: ${e.progress}%);
// },
});
`Use CDN:
Using jsDelivr CDN:
`html
`Using unpkg CDN:
`html
`$3
> This plugin let you eaisly mock requests
Usage:
with
GET:`ts
import xior from 'xior';
import MockPlugin from 'xior/plugins/mock';const instance = xior.create();
const mock = new MockPlugin(instance);
// Mock any GET request to /users
// arguments for reply are (status, data, headers)
mock.onGet('/users').reply(
200,
{
users: [{ id: 1, name: 'John Smith' }],
},
{
'X-Custom-Response-Header': '123',
}
);
instance.get('/users').then(function (response) {
console.log(response.data);
console.log(response.headers.get('X-Custom-Response-Header')); // 123
});
// Mock GET request to /users when param
searchText is 'John'
// arguments for reply are (status, data, headers)
mock.onGet('/users', { params: { searchText: 'John' } }).reply(200, {
users: [{ id: 1, name: 'John Smith' }],
});instance.get('/users', { params: { searchText: 'John' } }).then(function (response) {
console.log(response.data);
});
`with
POST:`ts
import xior from 'xior';
import MockPlugin from 'xior/plugins/mock';const instance = xior.create();
const mock = new MockPlugin(instance);
// Mock any POST request to /users
// arguments for reply are (status, data, headers)
mock.onPost('/users').reply(
200,
{
users: [{ id: 1, name: 'John Smith' }],
},
{
'X-Custom-Response-Header': '123',
}
);
instance.post('/users').then(function (response) {
console.log(response.data);
console.log(response.headers.get('X-Custom-Response-Header')); // 123
});
// Mock POST request to /users when param
searchText is 'John'
// arguments for reply are (status, data, headers)
mock.onPost('/users', null, { params: { searchText: 'John' } }).reply(200, {
users: [{ id: 1, name: 'John Smith' }],
});instance.get('/users', null, { params: { searchText: 'John' } }).then(function (response) {
console.log(response.data);
});
// Mock POST request to /users when body
searchText is 'John'
// arguments for reply are (status, data, headers)
mock.onPost('/users', { searchText: 'John' }).reply(200, {
users: [{ id: 1, name: 'John Smith' }],
});instance.get('/users', { searchText: 'John' }).then(function (response) {
console.log(response.data);
});
`More details, check here.
Use CDN:
Using jsDelivr CDN:
`html
`Using unpkg CDN:
`html
`$3
We will use
xior-auth-refresh plugin from the community: https://github.com/Audiu/xior-auth-refreshInstall:
`sh
npm install xior-auth-refresh --save
or
yarn add xior-auth-refresh
or
pnpm add xior-auth-refresh
`Usage:
`ts
import xior from 'xior';
import createAuthRefreshInterceptor from 'xior-auth-refresh';// Function that will be called to refresh authorization
const refreshAuthLogic = (failedRequest) =>
xior.post('https://www.example.com/auth/token/refresh').then((tokenRefreshResponse) => {
localStorage.setItem('token', tokenRefreshResponse.data.token);
failedRequest.response.config.headers['Authorization'] =
'Bearer ' + tokenRefreshResponse.data.token;
return Promise.resolve();
});
// Instantiate the interceptor
createAuthRefreshInterceptor(xior, refreshAuthLogic);
// Make a call. If it returns a 401 error, the refreshAuthLogic will be run,
// and the request retried with the new token
xior.get('https://www.example.com/restricted/area').then(/ ... /).catch(/ ... /);
`More: https://github.com/Audiu/xior-auth-refresh
$3
Usage:
`ts
import xior, { XiorResponse } from 'xior';
import errorRetry from 'xior/plugins/error-retry';
import setupTokenRefresh from 'xior/plugins/token-refresh';const instance = xior.create();
const TOKEN_KEY = 'TOKEN';
function getToken() {
return localStorage.getItem(TOKEN_KEY);
}
function setToken(token: string) {
return localStorage.setItem(TOKEN_KEY, token);
}
function deleteToken() {
return localStorage.getItem(TOKEN_KEY);
}
instance.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers['Authorization'] =
Bearer ${token};
}
return config;
});function shouldRefresh(response: XiorResponse) {
const token = getToken();
return Boolean(token && response?.status && [401, 403].includes(response.status));
}
instance.plugins.use(
errorRetry({
enableRetry: (config, error) => {
if (error?.response && shouldRefresh(error.response)) {
return true;
}
// return false
},
})
);
setupTokenRefresh(http, {
shouldRefresh,
async refreshToken(error) {
try {
const { data } = await http.post('/token/new');
if (data.token) {
setToken(data.token);
} else {
throw error;
}
} catch (e) {
// something wrong, delete old token
deleteToken();
return Promise.reject(error);
}
},
});
`$3
xior let you easily to create custom plugins.
Here are examples:
1. Simple Logging plugin:
`ts
import xior from 'xior';const instance = xior.create();
instance.plugins.use(function logPlugin(adapter, instance) {
return async (config) => {
const start = Date.now();
const res = await adapter(config);
console.log('%s %s %s take %sms', config.method, config.url, res.status, Date.now() - start);
return res;
};
});
`2. Check built-in plugins get more inspiration:
Check src/plugins
$3
`ts
import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
const http = xior.create();const pluginHandler = http.plugins.use(errorRetryPlugin());
http.plugins.eject(pluginHandler);
// Cleanup all plugins
// http.plugins.clear()
`Helper functions
xior has built-in helper functions, may useful for you:
`ts
import lru from 'tiny-lru';
import {
encodeParams,
merge as deepMerge,
delay as sleep,
buildSortedURL,
isAbsoluteURL,
joinPath,
isXiorError,
trimUndefined,
Xior,
} from 'xior';
`FAQ
xior frequently asked questions.
$3
No, but xior keeps a very similar API to axios. It supports things like
axios.create, axios.interceptors, and all the common HTTP methods (get, post, put, patch, delete, head, options).
Overall, itβs about 90% compatible.For most projects, the main change is just replacing
axios with xior and checking if your TypeScript types still pass.`ts
import axios, { AxiosError, isAxiosError, AxiosRequestConfig, AxiosResponse, isCancel } from 'xior';const instance = axios.create({
baseURL: '...',
timeout: 20_000,
});
// Get response headers
instance.get('/').then((response) => {
response.headers.get('x-response-time');
// In axios, this would be: response.headers['x-response-time']
});
`For a full guide, check Migrate-axios-to-xior.md.
$3
Yes, xior works anywhere where the native
fetch API is supported.
Even if the environment doesn't support fetch, you can use a fetch polyfill like for older browsers.For
Tauri or Taro: check 3. How can I use custom fetch implementation or How to support proxy feature?$3
To support proxy feature or custom fetch implementation, we can use
node-fetch, nodejs undici, or @tauri-apps/plugin-http module's fetch implementation to replace the built-in fetch.For example undici:
`sh
npm install undici
``ts
import { fetch as undiciFetch, FormData, Agent, type RequestInit as RequestInit_ } from 'undici';/ For TypeScript types /
declare global {
interface RequestInit extends RequestInit_ {}
}
/ Create Agent /
const agent = new Agent({
connections: 10,
});
const xiorInstance = xior.create({
baseURL: 'https://example.com',
fetch: undiciFetch,
dispatcher: agent,
});
`For example node-fetch:
`sh
For ESM module
npm install node-fetchFor CommonJS module
npm install node-fetch@v2.7.0
npm install @types/node-fetch -D
``ts
import nodeFetch, { RequestInit as RequestInit_ } from 'node-fetch';
import http from 'node:http';
import https from 'node:https';/ For TypeScript types /
declare global {
interface RequestInit extends RequestInit_ {}
}
/ Create Agent /
const httpAgent = new http.Agent({
keepAlive: true,
});
const httpsAgent = new https.Agent({
keepAlive: true,
});
const xiorInstance = xior.create({
baseURL: 'https://example.com',
fetch: nodeFetch,
// agent: httpAgent,
agent(_parsedURL) {
if (_parsedURL.protocol === 'http:') {
return httpAgent;
} else {
return httpsAgent;
}
},
});
`Use
@tauri-apps/plugin-http's fetch implementaion in Tauri:`ts
import { fetch } from '@tauri-apps/plugin-http';
import xior from 'xior';export const http = xior.create({
baseURL: 'https://www.tauri.app',
fetch,
});
async function test() {
const { data } = await http.get('/');
return data;
}
`For
Taro:`ts
import { fetch } from 'taro-fetch-polyfill';
import xior from 'xior';// fetch('https://api.github.com')
// .then(response => response.json())
// .then(console.log);
export const http = xior.create({
baseURL: 'https://github.com/NervJS/taro',
fetch,
});
async function test() {
const { data } = await http.get('/');
return data;
}
`$3
When
{responseType: 'blob'| 'arraybuffer'}:`ts
xior.get('https://exmaple.com/some/api', { responseType: 'blob' }).then((response) => {
console.log(response.data); // response.data is a Blob
});// Same with
fetch('https://exmaple.com/some/api')
.then((response) => response.blob())
.then((data) => {
console.log(data); // is a Blob
});
``ts
xior.get('https://exmaple.com/some/api', { responseType: 'arraybuffer' }).then((response) => {
console.log(response.data); // response.data is a ArrayBuffer
});// Same with
fetch('https://exmaple.com/some/api')
.then((response) => response.arraybuffer())
.then((data) => {
console.log(data); // is a ArrayBuffer
});
`But when
responseType set to 'document', 'custom' or 'original', Xior will return the original fetch response and res.data will be undefined:`ts
fetch('https://exmaple.com/some/api').then((response) => {
console.log(response);
});// same with
xior.get('https://exmaple.com/some/api', { responseType: 'stream' }).then((res) => {
console.log(res.response); // But res.data will be undefined
});
`$3
Use
validateResponse (not validateStatus), default validResponse is:`ts
function validateResponse(xiorResponse) {
return xiorResponse.response.ok;
}
`Custom yours:
`ts
import axios from 'xior';const client = axios.create({
validateResponse(xiorResponse) {
const { response, data } = xiorResponse;
if (response.ok && data?.code === '200') return true;
return response.ok;
},
});
`$3
You can use a polyfill for the
fetch API. Check the file src/tests/polyfill.test.ts for a potential example.$3
The original name
axior was unavailable on npm, so when removed the "a": ~~a~~xior.$3
If you have any questions, feel free to create issues.
Migrate from axios to xior
1. Main change: replace all
axios imports with xior and check TypeScript types.
`ts
import axios, { AxiosError, isAxiosError, AxiosRequestConfig, AxiosResponse } from 'xior'; const instance = axios.create({
baseURL: '...',
timeout: 20e3,
});
`2. Headers access: use
`ts
response.headers.get('X-Header-Name');
` instead of
`ts
response.headers['X-Header-Name'];
`3. Progress events:
xior doesnβt include upload/download progress by default. Use the plugin:
`ts
import Xior from 'xior';
import progressPlugin from 'xior/plugins/progress';
const axios = Xior.create({}); http.plugins.use(progressPlugin());
const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');
http.post('/upload', formData, {
onUploadProgress(e) {
console.log(
Upload progress: ${e.progress}%);
},
// progressDuration: 10 \* 1000
});
`4. Transforms:
xior does not support transformRequest or transformResponse.5. Network errors: in
xior, network errors are TypeError, not AxiosError with code or detail.6. Recommendation: keep
axios for legacy projects. Use xior for new or modern TypeScript projects.$3
axios:
`ts
import axios from 'axios';// Make a request for a user with a given ID
axios.get('/user?ID=12345');
// Optionally the request above could also be done as
axios.get('/user', {
params: {
ID: 12345,
},
});
// Want to use async/await? Add the
async keyword to your outer function/method.
async function getUser() {
try {
const response = await axios.get('/user?ID=12345');
console.log(response);
} catch (error) {
console.error(error);
}
}
`xior:
`ts
import axios from 'xior';// Make a request for a user with a given ID
axios.get('/user?ID=12345');
// Optionally the request above could also be done as
axios.get('/user', {
params: {
ID: 12345,
},
});
// Want to use async/await? Add the
async keyword to your outer function/method.
async function getUser() {
try {
const response = await axios.get('/user?ID=12345');
console.log(response);
} catch (error) {
console.error(error);
}
}
`$3
axios:
`ts
import axios from 'axios';axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone',
});
`xior:
`ts
import axios from 'xior';axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone',
});
`$3
axios:
`ts
import axios from 'axios';await axios({ method: 'get', params: { a: 1 } });
`xior:
`ts
import xior from 'xior';const axios = xior.create();
await axios({ method: 'get', params: { a: 1 } });
`$3
axios:
`ts
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
});
`xior:
`ts
import axios from 'xior';
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
});
`$3
axios:
`ts
import axios from 'axios';const axiosInstance = axios.create({
baseURL: 'https://apiexampledomian.com/api',
});
const { data, headers } = await axiosInstance.get('/');
console.log(headers['X-Header-Name']);
`xior:
`ts
import xior from 'xior';const xiorInstance = xior.create({
baseURL: 'https://apiexampledomian.com/api',
});
const { data, headers } = await xiorInstance.get('/');
console.log(headers.get('X-Header-Name'));
`$3
Ref: https://github.com/suhaotian/xior/issues/21
transformRequest This property is unnecessary and useless in xior.js.
You can modify the payload in request interceptors, or directly before calling the request function.`ts
function requestAPI(payload: any) {
// modify payload here
const newPayload = {
...payload,
};
return xiorInstance.post('/api', newPayload);
}
`Or change
data or headers in request interceptors:`ts
xiorInstance.interceptors.request.use((config) => {
if (config.url === '/endpoint') {
delete config.headers['Content-Type'];
}
/*
or delete the Content-Type if the data is FormData,
because if it's formData, browser will automatically add headers
*/
// if (config.data instanceof FormData) {
// delete config.headers['Content-Type']
// }
return config;
});
`$3
transformResponse this property is unnecessary and useless in xior.js.
You can transform the response directly in a .then() callback.`ts
xiorInstance.get('/api').then((response) => {
// transform response data here
return response;
});
`$3
axios:
`ts
import axios from 'axios';
import fs from 'fs';// GET request for remote image in Node.js
axios({
method: 'get',
url: 'https://bit.ly/2mTM3nY',
responseType: 'stream',
}).then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
});
// For browser
axios({
method: 'get',
url: 'https://bit.ly/2mTM3nY',
responseType: 'blob',
}).then(function (response) {
// create file link in browser's memory
const href = URL.createObjectURL(response.data);
// create "a" HTML element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'file.pdf'); //or any other extension
document.body.appendChild(link);
link.click();
// clean up "a" element & remove ObjectURL
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
`xior:
`ts
// Node.js
import xior from 'xior';
import fs from 'fs';
const axios = xior.create();axios
.get('https://bit.ly/2mTM3nY', {
responseType: 'stream',
})
.then(async function ({ response, config }) {
const buffer = Buffer.from(await response.arrayBuffer());
return fs.writeFile('ada_lovelace.jpg', buffer);
});
// GET request for remote image in Node.js, Same with axios
responseType: 'stream'.
axios
.request({
method: 'get',
url: 'https://bit.ly/2mTM3nY',
responseType: 'stream',
})
.then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
});// For browser
xior
.get('https://d2l.ai/d2l-en.pdf', {
headers: {
Accept: 'application/pdf',
},
responseType: 'blob',
})
.then((res) => {
const { data: blob } = res;
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'filename.pdf';
document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
a.click();
a.remove(); //afterwards we remove the element again
});
`$3
axios:
`ts
import axios from 'axios';const http = axios.create();
async function getStream(url: string, params: Record) {
const { data } = await http.get(url, {
params,
responseType: 'stream',
});
return data;
}
`xior with stream plugin at Node.js:
`ts
import axios from 'xior';const http = axios.create();
async function getStream(url: string, params: Record) {
const { data } = await http.get(url, {
params,
responseType: 'stream',
});
const stream = data;
return stream;
}
`Migrate from
fetch to xior$3
fetch:
`ts
async function logMovies() {
const response = await fetch('http://example.com/movies.json?page=1&perPage=10');
const movies = await response.json();
console.log(movies);
}
`xior:
`ts
import xior from 'xior';const http = xior.create({
baseURL: 'http://example.com',
});
async function logMovies() {
const { data: movies } = await http.get('/movies.json', {
params: {
page: 1,
perPage: 10,
},
});
console.log(movies);
}
`$3
fetch:
`ts
// Example POST method implementation:
async function postData(url = '', data = {}) {
// Default options are marked with *
const response = await fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
// 'Content-Type': 'application/json',
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data), // body data type must match "Content-Type" header
});
return response.json(); // parses JSON response into native JavaScript objects
}postData('https://example.com/answer', { answer: 42 }).then((data) => {
console.log(data); // JSON data parsed by
data.json() call
});
`xior:
`ts
import xior from 'xior';const http = xior.create({
baseURL: 'http://example.com',
});
http
.post(
'/answer',
{ answer: 42 },
{
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
// 'Content-Type': 'application/json',
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
}
)
.then(({ data }) => {
console.log(data);
});
`$3
fetch:
`ts
const controller = new AbortController();
const signal = controller.signal;
const url = 'video.mp4';const downloadBtn = document.querySelector('#download');
const abortBtn = document.querySelector('#abort');
downloadBtn.addEventListener('click', async () => {
try {
const response = await fetch(url, { signal });
console.log('Download complete', response);
} catch (error) {
console.error(
Download error: ${error.message});
}
});abortBtn.addEventListener('click', () => {
controller.abort();
console.log('Download aborted');
});
`xior:
`ts
import xior from 'xior';const http = xior.create();
const controller = new AbortController();
const signal = controller.signal;
const url = 'video.mp4';
const downloadBtn = document.querySelector('#download');
const abortBtn = document.querySelector('#abort');
downloadBtn.addEventListener('click', async () => {
try {
const response = await http.get(url, { signal });
console.log('Download complete', response);
} catch (error) {
console.error(
Download error: ${error.message});
}
});abortBtn.addEventListener('click', () => {
controller.abort();
console.log('Download aborted');
});
`$3
fetch:
`ts
fetch('https://example.com', {
credentials: 'include',
});
`xior:
`ts
import xior from 'xior';const http = xior.create();
http.get('https://example.com', {
credentials: 'include',
});
`$3
fetch:
`ts
async function upload(formData) {
try {
const response = await fetch('https://example.com/profile/avatar', {
method: 'PUT',
body: formData,
});
const result = await response.json();
console.log('Success:', result);
} catch (error) {
console.error('Error:', error);
}
}const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);
upload(formData);
`xior:
`ts
import xior from 'xior';const http = xior.create({
baseURL: 'https://example.com',
});
async function upload(formData) {
try {
const { data: result } = await http.put('/profile/avatar', formData);
console.log('Success:', result);
} catch (error) {
console.error('Error:', error);
}
}
const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);
upload(formData);
`$3
fetch:
`ts
async function* makeTextFileLineIterator(fileURL) {
const utf8Decoder = new TextDecoder('utf-8');
const response = await fetch(fileURL);
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk) : ''; const newline = /\r?\n/gm;
let startIndex = 0;
let result;
while (true) {
const result = newline.exec(chunk);
if (!result) {
if (readerDone) break;
const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
startIndex = newline.lastIndex = 0;
continue;
}
yield chunk.substring(startIndex, result.index);
startIndex = newline.lastIndex;
}
if (startIndex < chunk.length) {
// Last line didn't end in a newline char
yield chunk.substr(startIndex);
}
}
async function run() {
for await (const line of makeTextFileLineIterator(urlOfFile)) {
processLine(line);
}
}
run();
`xior:
> Good to Know: add
{responseType: 'original'} options will tell xior no need process response, and return original response in format {response}`ts
import xior from 'xior';const http = xior.create();
async function* makeTextFileLineIterator(fileURL) {
const utf8Decoder = new TextDecoder('utf-8');
const { response } = await http.get(fileURL, { responseType: 'original' });
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk) : '';
const newline = /\r?\n/gm;
let startIndex = 0;
let result;
while (true) {
const result = newline.exec(chunk);
if (!result) {
if (readerDone) break;
const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
startIndex = newline.lastIndex = 0;
continue;
}
yield chunk.substring(startIndex, result.index);
startIndex = newline.lastIndex;
}
if (startIndex < chunk.length) {
// Last line didn't end in a newline char
yield chunk.substr(startIndex);
}
}
async function run() {
for await (const line of makeTextFileLineIterator(urlOfFile)) {
processLine(line);
}
}
run();
``- https://www.jsdocs.io/package/xior

Without the support of these resources, xior wouldn't be possible:
- axios
- axios-extensions
- axios-mock-adapter
- ~~ts-deepmerge~~
- tiny-lru
- ~~bunchee~~ tsup
- fetch MDN docs