Axly is a powerful and flexible HTTP client library built on top of Axios, designed for seamless API interactions in both browser and Node.js environments. It provides automatic token refreshing, retry with exponential backoff, upload/download progress tracking, request deduplication, response caching, toast notifications (browser-only), request cancellation, and support for multiple API configurations.
- Migrating from v2 to v3
- Features
- Installation
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Advanced Features
- TypeScript Support
- Error Classes
- Best Practices
- Contributing
- License
v3 is a breaking release. The high-level API is unchanged β createAxlyClient, useAxly, useAxlyQuery, useAxlyMutation all work the same β but a handful of methods were renamed or consolidated.
invalidate replaces clearCache and gains pattern-matching.
// v2
client.clearCache();
client.clearCache('mainAPI');
// v3
client.invalidate();
client.invalidate({ configId: 'mainAPI' });
client.invalidate({ url: /\/users\// });
client.invalidate({ predicate: (key) => key.includes('list') });They've been merged β setAccessToken now updates storage and the axios default header.
// v2
client.setAuthorizationHeader(token);
// v3
client.setAccessToken(token);The unused middle generic was removed:
// v2
AxlyMutationOptions<T, D, C>;
// v3
AxlyMutationOptions<T, C>;The useAxlyMutation<T, D, C>(...) hook signature itself is unchanged β only AxlyMutationOptions dropped the middle generic.
v2 retried any error except cancellation. v3 only retries on:
- Network errors (
ERR_NETWORK,ECONNABORTED,ETIMEDOUT) - HTTP 5xx
- HTTP 408 and 429
To restore v2 behavior, pass shouldRetry: () => true on the config or per-request.
upload() is now implemented in terms of request() and inherits retry behavior. To preserve v2 (no retries), pass retry: 0:
client.upload(url, formData, { retry: 0 });authSchemeβAxlyConfig.authScheme?: string | null(default'Bearer'). Passnullor''to send the token raw (e.g. forToken abc123schemes).shouldRetryβ custom retry predicate on bothAxlyConfigandRequestOptions.staleWhileRevalidateβ extendCacheOptions.staleWhileRevalidate(ms) to serve stale responses while refreshing in the background.invalidate({ url, predicate })β pattern-based cache invalidation.
- Cache-key normalization β reordered query params now share the same cache entry (they produced different keys in v2).
- 401 handling triggers refresh on the first 401 instead of after exhausting retries.
- The internal cache-sweep timer now
.unref()'s, so Node processes exit naturally without callingdestroy().
- π Axios Integration β Reliable HTTP requests with full interceptor support
- π Multiple Configurations β Multiple API configs with different base URLs and auth setups
- βοΈ React Hooks β
useAxly,useAxlyQuery, anduseAxlyMutationfor managing state - π Token Management β Access and refresh tokens with automatic refreshing on 401 errors
- π Automatic Retries β Exponential backoff with jitter for transient failures
- β»οΈ Request Deduplication β Prevents duplicate concurrent requests for the same resource
- πΎ Response Caching β TTL-based caching for GET requests, configurable per-request
- π Progress Tracking β Real-time upload and download progress monitoring
- π¨ Toast Notifications β Customizable success/error toasts (browser-only)
- β Request Cancellation β Abort ongoing requests via
AbortController - π File Uploads β Simplified file uploads using
FormData β οΈ Error Handling β Custom error handlers and typed error classes- π₯οΈ Node.js Support β
createAxlyNodeClientwith server-optimized defaults - π TypeScript β Full type safety with comprehensive generics
npm install axly
# or
yarn add axly
# or
pnpm add axly
# or
bun add axlyPeer dependencies: React (
>=18) is optional and only needed for the React hooks (useAxly,useAxlyQuery,useAxlyMutation).
// apiClient.ts
import { createAxlyClient } from 'axly';
const apiClient = createAxlyClient({
baseURL: 'https://api.example.com',
token: localStorage.getItem('authToken'),
toastHandler: (msg, type) => console.log(`[${type}]`, msg)
});
export default apiClient;import { useAxly } from 'axly';
import apiClient from './apiClient';
const CreateUser = () => {
const { isLoading, status, request } = useAxly(apiClient);
const handleCreate = async () => {
const response = await request({
method: 'POST',
url: '/users',
data: { name: 'Jane Doe', email: '[email protected]' }
});
console.log(response.data);
};
return (
<button onClick={handleCreate} disabled={isLoading}>
{status === 'loading' ? 'Creating...' : 'Create User'}
</button>
);
};import { useAxlyQuery } from 'axly';
import apiClient from './apiClient';
const UserList = () => {
const { data, isLoading, error, refetch } = useAxlyQuery({
client: apiClient,
request: { method: 'GET', url: '/users' }
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data?.data.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
};import { createAxlyClient } from 'axly';
const apiClient = createAxlyClient({
baseURL: 'https://api.example.com',
token: 'your-jwt-token',
toastHandler: (message, type) => {
console.log(`[${type}] ${message}`);
}
});import { createAxlyClient } from 'axly';
const client = createAxlyClient({
mainAPI: {
baseURL: 'https://api.example.com',
multiToken: true,
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken'),
refreshEndpoint: '/auth/refresh',
onRefresh: ({ accessToken, refreshToken }) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
},
onRefreshFail: () => {
window.location.href = '/login';
}
},
publicAPI: {
baseURL: 'https://public.example.com'
}
});
// Use a specific config
await client.request({ method: 'GET', url: '/data', configId: 'publicAPI' });Creates an Axly client with one or more configurations.
createAxlyClient<ConfigMap>(config: AxlyConfig | ConfigMap): AxlyClient| Option | Type | Default | Description |
|---|---|---|---|
baseURL |
string |
β | Base URL for all requests (required) |
token |
string | null |
β | Single auth token (single-token mode) |
multiToken |
boolean |
false |
Enable access + refresh token mode |
accessToken |
string | null |
β | Access token for multi-token mode |
refreshToken |
string | null |
β | Refresh token for multi-token mode |
refreshEndpoint |
string |
β | Endpoint for token refresh |
refreshTimeout |
number |
10000 |
Timeout (ms) for refresh requests |
authScheme |
string | null |
'Bearer' |
Prefix for the Authorization header. null/'' sends the token raw |
toastHandler |
ToastHandler |
β | Toast notification function (browser-only) |
tokenCallbacks |
TokenCallbacks |
β | Custom getters/setters for tokens |
requestInterceptors |
Array |
β | Axios request interceptors |
responseInterceptors |
Array |
β | Axios response interceptors |
errorHandler |
Function |
β | Custom error handler for all requests |
onRefresh |
Function |
β | Callback when tokens refresh |
onRefreshFail |
Function |
β | Callback when token refresh fails |
dedupeRequests |
boolean |
false |
Deduplicate identical concurrent GET requests |
shouldRetry |
ShouldRetry | undef. |
β | Predicate (err, attempt) => boolean. Defaults to network + 5xx + 408/429. |
Same as createAxlyClient but strips toastHandler for Node.js compatibility.
createAxlyNodeClient<ConfigMap>(config: AxlyConfig | ConfigMap): AxlyClientMake an HTTP request.
const response = await client.request<User>({
method: 'GET',
url: '/users/1'
});| Option | Type | Default | Description |
|---|---|---|---|
method |
string |
β | HTTP method (required) |
url |
string |
β | Endpoint URL (required) |
data |
D |
β | Request body |
params |
Record<string, ...> |
β | Query parameters |
contentType |
ContentType |
application/json |
Content-Type header |
customHeaders |
Record<string, string> |
β | Additional headers |
responseType |
string |
json |
Axios response type |
baseURL |
string |
β | Override the client base URL |
timeout |
number |
100000 |
Request timeout (ms) |
retry |
number |
0 |
Number of retry attempts |
cancelable |
boolean |
false |
Enable abort via AbortController |
onCancel |
() => void |
β | Callback when request is cancelled |
dedupe |
boolean |
false |
Deduplicate this GET request if another identical one is in-flight |
cache |
boolean | CacheOptions |
false |
Cache GET response; { ttl?, staleWhileRevalidate? } in ms |
shouldRetry |
ShouldRetry |
β | Per-request retry predicate (overrides config) |
successToast |
boolean |
false |
Show success toast |
errorToast |
boolean |
false |
Show error toast |
customToastMessage |
string |
β | Override success toast message |
customToastMessageType |
CustomToastMessageType |
success |
Toast type for success |
customErrorToastMessage |
string |
β | Override error toast message |
customErrorToastMessageType |
CustomToastMessageType |
error |
Toast type for error |
onUploadProgress |
(percent: number) => void |
β | Upload progress callback |
onDownloadProgress |
(percent: number) => void |
β | Download progress callback |
toastHandler |
ToastHandler |
β | Override the client toast handler per-request |
configId |
string |
default |
Which config to use in multi-config setup |
Upload a file. upload() is a thin wrapper around request() β it inherits retry, toast, dedupe, and state-tracking. It does not set Content-Type explicitly; axios auto-sets multipart/form-data; boundary=... when it detects FormData.
const form = new FormData();
form.append('file', file);
const response = await client.upload<{ url: string }>('/upload', form, {
onUploadProgress: (percent) => console.log(`${percent}%`)
});| Option | Type | Description |
|---|---|---|
headers |
Record<string, string> |
Additional request headers |
timeout |
number |
Request timeout in ms (default 120_000) |
onUploadProgress |
(percent: number) => void |
Upload progress callback |
onDownloadProgress |
(percent: number) => void |
Download progress callback |
baseURL |
string |
Override the client base URL |
cancelable |
boolean |
Enable abort via AbortController |
onCancel |
() => void |
Callback when upload is cancelled |
configId |
string |
Which config to use in multi-config setup |
retry |
number |
Retry attempts (pass 0 for v2-style no retries) |
shouldRetry |
ShouldRetry |
Custom retry predicate |
toastHandler |
ToastHandler |
Override the client toast handler |
successToast |
boolean |
Show success toast |
errorToast |
boolean |
Show error toast |
customToastMessage |
string |
Override success toast message |
customToastMessageType |
CustomToastMessageType |
Toast type for success |
customErrorToastMessage |
string |
Override error toast message |
customErrorToastMessageType |
CustomToastMessageType |
Toast type for error |
client.setAccessToken('new-token', 'configId'); // Set access token (updates storage + axios defaults)
client.setRefreshToken('refresh-token', 'configId'); // Set refresh token
client.setDefaultHeader('X-Custom', 'value', 'configId'); // Set a default header
client.clearDefaultHeader('X-Custom', 'configId'); // Remove a default headerclient.invalidate(); // Clear everything (all configs)
client.invalidate({ configId: 'mainAPI' }); // Clear all entries for one config
client.invalidate({ url: '/users' }); // Clear entries whose key contains '/users'
client.invalidate({ url: /\/users\/\d+/ }); // Regex match against cache keys
client.invalidate({ predicate: (key) => key.startsWith('GET:') }); // Arbitrary predicateWhen multiple fields are provided, they combine with AND semantics (all matchers must pass). Invalidation clears both the response cache and any in-flight deduped requests.
client.cancelRequest(abortController); // Cancel a specific request
client.destroy(); // Cleanup all instances, caches, and timers
client.on('destroy', () => {}); // Listen to eventsGeneral-purpose React hook for imperative requests with loading state.
const {
request,
cancelRequest,
isLoading,
status,
uploadProgress,
downloadProgress
} = useAxly(client);| Return value | Type | Description |
|---|---|---|
request |
Function |
Make a request (same signature as client) |
cancelRequest |
() => void |
Cancel the current in-flight request |
isLoading |
boolean |
true while request is in flight |
status |
RequestStatus |
'idle' | 'loading' | 'success' | 'error' |
uploadProgress |
number |
Upload progress percentage (0β100) |
downloadProgress |
number |
Download progress percentage (0β100) |
Declarative data-fetching hook. Auto-fetches on mount, supports polling and refetch.
const { data, error, status, isLoading, isFetching, refetch } = useAxlyQuery({
client,
request: { method: 'GET', url: '/users' },
enabled: true,
refetchOnMount: true,
refetchInterval: 30000, // poll every 30s
onSuccess: (response) => console.log(response.data),
onError: (error) => console.error(error)
});| Option | Type | Default | Description |
|---|---|---|---|
client |
AxlyClient |
β | Axly client instance (required) |
request |
RequestOptions |
β | Request configuration (required) |
enabled |
boolean |
true |
Skip fetch when false |
refetchOnMount |
boolean |
true |
Fetch when component mounts |
refetchInterval |
number | false |
false |
Poll interval in ms; false to disable |
onSuccess |
(res: AxiosResponse) => void |
β | Called on successful fetch |
onError |
(err: Error) => void |
β | Called on fetch failure |
| Property | Type | Description |
|---|---|---|
data |
AxiosResponse | null |
Last successful response |
error |
Error | null |
Last error |
status |
RequestStatus |
'idle' | 'loading' | 'success' | 'error' |
isLoading |
boolean |
true on first load (no data yet) |
isFetching |
boolean |
true on any fetch (including refetch) |
refetch |
() => Promise<void> |
Manually trigger a refetch |
Hook for mutations (POST, PUT, PATCH, DELETE) with mutate / mutateAsync.
const { mutate, mutateAsync, isPending, data, error, status, reset } =
useAxlyMutation({
client,
onSuccess: (response) => console.log('Done:', response.data),
onError: (error) => console.error('Failed:', error),
onSettled: (data, error) => console.log('Settled')
});
// Fire-and-forget
mutate({ method: 'POST', url: '/users', data: { name: 'Jane' } });
// Async with result
const response = await mutateAsync({
method: 'POST',
url: '/users',
data: { name: 'Jane' }
});| Option | Type | Description |
|---|---|---|
client |
AxlyClient |
Axly client instance (required) |
onSuccess |
(res: AxiosResponse) => void |
Called on success |
onError |
(err: Error) => void |
Called on error |
onSettled |
(res, err) => void |
Called on both success and error |
| Property | Type | Description |
|---|---|---|
mutate |
Function |
Trigger mutation (fire-and-forget) |
mutateAsync |
Function |
Trigger mutation and return a Promise |
isPending |
boolean |
true while mutation is in flight |
data |
AxiosResponse | null |
Last successful response |
error |
Error | null |
Last error |
status |
RequestStatus |
'idle' | 'loading' | 'success' | 'error' |
reset |
() => void |
Reset state back to idle |
// GET
const { data } = await client.request<User[]>({ method: 'GET', url: '/users' });
// POST
await client.request({ method: 'POST', url: '/users', data: { name: 'Jane' } });
// With query params
await client.request({
method: 'GET',
url: '/users',
params: { page: '1', limit: '20' }
});// Single token (simple auth)
const client = createAxlyClient({
baseURL: 'https://api.example.com',
token: localStorage.getItem('token')
});
// Update token at runtime
client.setAccessToken('new-token');
// Multi-token with auto-refresh
const client = createAxlyClient({
baseURL: 'https://api.example.com',
multiToken: true,
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken'),
refreshEndpoint: '/auth/refresh',
onRefresh: ({ accessToken, refreshToken }) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
},
onRefreshFail: () => {
window.location.href = '/login';
}
});Prevent identical concurrent GET requests from firing multiple times.
// Enable globally for a config
const client = createAxlyClient({
baseURL: 'https://api.example.com',
dedupeRequests: true
});
// Or per-request
await client.request({ method: 'GET', url: '/users', dedupe: true });
// Both of these share a single network request:
const [a, b] = await Promise.all([
client.request({ method: 'GET', url: '/users', dedupe: true }),
client.request({ method: 'GET', url: '/users', dedupe: true })
]);Cache GET responses with a configurable TTL (default: 5 minutes).
// Cache with default TTL (5 minutes)
await client.request({ method: 'GET', url: '/config', cache: true });
// Cache with custom TTL (30 seconds)
await client.request({
method: 'GET',
url: '/config',
cache: { ttl: 30_000 }
});
// Combine caching with deduplication
await client.request({
method: 'GET',
url: '/config',
cache: { ttl: 60_000 },
dedupe: true
});Serve a stale cached response immediately while refreshing in the background:
// Fresh for 60s; serve stale for up to 5 more minutes while refreshing
await client.request({
method: 'GET',
url: '/config',
cache: {
ttl: 60_000,
staleWhileRevalidate: 300_000
}
});Semantics:
now < expiresAtβ return cached response (fresh)now < expiresAt + staleWhileRevalidateβ return cached response (stale) AND fire a background refresh; errors from the background refresh are swallowed- Otherwise β full network request
The background refresh is deduplicated per cache key β only one refresh runs at a time even under concurrent reads.
Clear cached entries by URL pattern, config, or arbitrary predicate:
// After mutating a user, clear all cached GETs that touch that path
await client.request({
method: 'PATCH',
url: '/users/42',
data: { name: 'Jane' }
});
client.invalidate({ url: '/users/42' });
// Clear every /users/* entry
client.invalidate({ url: /\/users\// });
// Clear by config
client.invalidate({ configId: 'mainAPI' });
// Arbitrary predicate β e.g. drop every non-GET entry (shouldn't exist, but illustrative)
client.invalidate({ predicate: (key) => !key.startsWith('GET:') });
// Clear everything (cache + in-flight dedupes, all configs)
client.invalidate();const { request, uploadProgress, downloadProgress } = useAxly(client);
await request({
method: 'GET',
url: '/large-file',
responseType: 'blob',
onDownloadProgress: (percent) => console.log(`Download: ${percent}%`)
});const handleUpload = async (file: File) => {
const form = new FormData();
form.append('file', file);
const response = await client.upload<{ url: string }>('/upload', form, {
onUploadProgress: (percent) => setProgress(percent)
});
console.log('Uploaded to:', response.data.url);
};const { request, cancelRequest, isLoading } = useAxly(client);
const search = async (query: string) => {
await request({
method: 'GET',
url: '/search',
params: { q: query },
cancelable: true,
onCancel: () => console.log('Search cancelled')
});
};
// Cancel any in-flight request
<button onClick={cancelRequest} disabled={!isLoading}>
Cancel
</button>;By default, axly retries on transient failures only: network errors (ERR_NETWORK, ECONNABORTED, ETIMEDOUT), HTTP 5xx, HTTP 408, and HTTP 429.
// Retry up to 3 times with exponential backoff + jitter
await client.request({
method: 'GET',
url: '/unstable-endpoint',
retry: 3
});
// Delays: ~500ms, ~1000ms, ~2000ms (with random jitter)Take full control over which errors retry and which fail fast:
// Per-request: retry only on 503, up to 5 times
await client.request({
method: 'GET',
url: '/flaky',
retry: 5,
shouldRetry: (err, attempt) => err.response?.status === 503
});
// Per-config: retry everything except 4xx (v2-style aggressive retrying, but skip auth/validation errors)
const client = createAxlyClient({
baseURL: 'https://api.example.com',
shouldRetry: (err) => {
const status = err.response?.status;
if (status != null && status >= 400 && status < 500) return false;
return true;
}
});
// Restore exact v2 behavior (retry on any error)
const legacyClient = createAxlyClient({
baseURL: 'https://api.example.com',
shouldRetry: () => true
});Precedence: per-request shouldRetry > per-config shouldRetry > default predicate.
When multiToken + refreshEndpoint are configured and the server returns 401, axly refreshes the token on the first 401 (not after exhausting retries) and then retries the request with the fresh token. Retries after a successful refresh use your normal retry/shouldRetry budget.
The default Authorization header is Bearer <token>. Override with authScheme:
// Default β emits "Authorization: Bearer abc123"
const client = createAxlyClient({
baseURL: 'https://api.example.com',
token: 'abc123'
});
// GitHub-style β emits "Authorization: token abc123"
const githubClient = createAxlyClient({
baseURL: 'https://api.github.com',
token: 'abc123',
authScheme: 'token'
});
// Raw token β emits "Authorization: abc123" (no prefix)
const rawClient = createAxlyClient({
baseURL: 'https://api.example.com',
token: 'abc123',
authScheme: null
});
// AWS-style β emits "Authorization: AWS4-HMAC-SHA256 <sig>"
const awsClient = createAxlyClient({
baseURL: 'https://s3.example.com',
token: '<sig>',
authScheme: 'AWS4-HMAC-SHA256'
});authScheme applies to both the initial header and any headers written by setAccessToken / automatic refresh flows.
const client = createAxlyClient({
baseURL: 'https://api.example.com',
toastHandler: (message, type) => {
// Use any toast library
toast[type ?? 'info'](message);
}
});
await client.request({
method: 'POST',
url: '/users',
data: { name: 'Jane' },
successToast: true,
errorToast: true,
customToastMessage: 'User created successfully!'
});import { RequestError, AuthError, CancelledError } from 'axly';
try {
await client.request({ method: 'GET', url: '/protected' });
} catch (err) {
if (err instanceof CancelledError) {
console.log('Request was cancelled');
} else if (err instanceof AuthError) {
console.log('Auth failed β redirect to login');
} else if (err instanceof RequestError) {
console.log('Status:', err.response?.status);
console.log('Code:', err.code);
}
}const client = createAxlyClient({
userAPI: { baseURL: 'https://users.example.com', token: 'user-token' },
paymentAPI: { baseURL: 'https://pay.example.com', token: 'pay-token' }
});
const users = await client.request({
method: 'GET',
url: '/list',
configId: 'userAPI'
});
const invoices = await client.request({
method: 'GET',
url: '/invoices',
configId: 'paymentAPI'
});const client = createAxlyClient({
baseURL: 'https://api.example.com',
requestInterceptors: [
(config) => {
config.headers['X-Request-ID'] = crypto.randomUUID();
return config;
}
],
responseInterceptors: [
(response) => {
console.log('Response time:', response.headers['x-response-time']);
return response;
}
]
});import { createAxlyNodeClient } from 'axly';
const client = createAxlyNodeClient({
baseURL: 'https://api.example.com',
token: process.env.API_TOKEN
});
const { data } = await client.request<User[]>({ method: 'GET', url: '/users' });When multiToken: true and a request returns 401, Axly automatically:
- Calls your
refreshEndpointwith the current refresh token - Updates access and refresh tokens (via
tokenCallbacksor in-memory) - Retries the failed request with the new access token
- Prevents duplicate concurrent refresh calls
const client = createAxlyClient({
baseURL: 'https://api.example.com',
multiToken: true,
refreshEndpoint: '/auth/refresh',
tokenCallbacks: {
getAccessToken: () => localStorage.getItem('accessToken'),
setAccessToken: (token) =>
token ?
localStorage.setItem('accessToken', token)
: localStorage.removeItem('accessToken'),
getRefreshToken: () => localStorage.getItem('refreshToken'),
setRefreshToken: (token) =>
token ?
localStorage.setItem('refreshToken', token)
: localStorage.removeItem('refreshToken')
}
});Use tokenCallbacks to manage tokens in any external storage (localStorage, Redux, Zustand, etc.):
import { useAuthStore } from './store';
const client = createAxlyClient({
baseURL: 'https://api.example.com',
multiToken: true,
tokenCallbacks: {
getAccessToken: () => useAuthStore.getState().accessToken,
setAccessToken: (t) => useAuthStore.getState().setAccessToken(t),
getRefreshToken: () => useAuthStore.getState().refreshToken,
setRefreshToken: (t) => useAuthStore.getState().setRefreshToken(t)
}
});const unsubscribe = client.on('destroy', () => {
console.log('Client destroyed β clearing auth state');
});
// Later: stop listening
unsubscribe();
// Destroy the client (clears all caches, deduplication maps, axios instances)
client.destroy();Axly is written in TypeScript with full generic support.
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserPayload {
name: string;
email: string;
}
// Fully typed request
const response = await client.request<User, CreateUserPayload>({
method: 'POST',
url: '/users',
data: { name: 'Jane', email: '[email protected]' }
});
const user: User = response.data;
// Typed multi-config client
const client = createAxlyClient({
users: { baseURL: 'https://users.example.com' },
billing: { baseURL: 'https://billing.example.com' }
});
// configId is narrowed to 'users' | 'billing'
await client.request({ method: 'GET', url: '/list', configId: 'users' });
// Typed hooks
const { data } = useAxlyQuery<User[]>({
client,
request: { method: 'GET', url: '/users' }
});
// data?.data is User[] | undefinedAdditional types exported from axly:
import type {
AxlyClient,
AxlyConfig,
RequestOptions,
UploadOptions,
InvalidateOptions,
CacheOptions,
ShouldRetry,
TokenCallbacks,
RefreshTokens,
StateData,
RequestStatus,
ToastHandler,
AxlyQueryOptions,
AxlyQueryResult,
AxlyMutationOptions,
AxlyMutationResult
} from 'axly';
// ShouldRetry signature
const mySRetry: ShouldRetry = (err, attempt) => {
return err.response?.status === 503 && attempt < 2;
};| Class | When thrown |
|---|---|
RequestError |
HTTP request fails after all retries |
AuthError |
Token refresh fails or auth is missing |
CancelledError |
Request was aborted via AbortController |
import { RequestError, AuthError, CancelledError } from 'axly';
// RequestError properties
err.message; // Error message
err.response; // AxiosResponse | null
err.code; // HTTP status code or Axios error code
err.original; // Original AxiosErrorCreate one client per API β share it via a module export or React context, not recreated per component.
Use dedupeRequests: true on configs that serve shared data (e.g., user profile, feature flags) to avoid redundant network calls.
Use cache for data that rarely changes (config endpoints, reference data) and refetchInterval in useAxlyQuery for live data.
Use useAxlyQuery for GET requests that should auto-fetch, and useAxlyMutation for writes. Reach for useAxly only when you need full imperative control.
Handle AuthError globally β attach a listener on the client or set onRefreshFail to redirect to login.
Prefer tokenCallbacks over passing tokens directly to keep the client decoupled from your storage strategy.
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
- Fork the repository
- Create your feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -m 'Add my feature' - Push to the branch:
git push origin feature/my-feature - Open a pull request
MIT Β© Harshal Katakiya