Skip to content

Harshalkatakiya/axly

Repository files navigation

Axly

npm version npm downloads License: MIT TypeScript

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.


πŸ“‹ Table of Contents


🚨 Migrating from v2 to v3

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.

1. clearCache β†’ invalidate

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') });

2. setAuthorizationHeader β†’ setAccessToken

They've been merged β€” setAccessToken now updates storage and the axios default header.

// v2
client.setAuthorizationHeader(token);

// v3
client.setAccessToken(token);

3. AxlyMutationOptions generic signature

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.

4. Retry defaults

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.

5. Upload retries

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 });

New features in v3

  • authScheme β€” AxlyConfig.authScheme?: string | null (default 'Bearer'). Pass null or '' to send the token raw (e.g. for Token abc123 schemes).
  • shouldRetry β€” custom retry predicate on both AxlyConfig and RequestOptions.
  • staleWhileRevalidate β€” extend CacheOptions.staleWhileRevalidate (ms) to serve stale responses while refreshing in the background.
  • invalidate({ url, predicate }) β€” pattern-based cache invalidation.

Behavioral fixes (no API change)

  • 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 calling destroy().

✨ Features

  • πŸ”Œ 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, and useAxlyMutation for 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 β€” createAxlyNodeClient with server-optimized defaults
  • πŸ“˜ TypeScript β€” Full type safety with comprehensive generics

πŸ“¦ Installation

npm install axly
# or
yarn add axly
# or
pnpm add axly
# or
bun add axly

Peer dependencies: React (>=18) is optional and only needed for the React hooks (useAxly, useAxlyQuery, useAxlyMutation).


πŸš€ Quick Start

Basic Setup

// 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;

Using the useAxly hook

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>
  );
};

Data fetching with useAxlyQuery

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>
  );
};

🎯 Core Concepts

Single Configuration

import { createAxlyClient } from 'axly';

const apiClient = createAxlyClient({
  baseURL: 'https://api.example.com',
  token: 'your-jwt-token',
  toastHandler: (message, type) => {
    console.log(`[${type}] ${message}`);
  }
});

Multiple Configurations

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' });

πŸ“š API Reference

createAxlyClient

Creates an Axly client with one or more configurations.

createAxlyClient<ConfigMap>(config: AxlyConfig | ConfigMap): AxlyClient

AxlyConfig Options

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.

createAxlyNodeClient

Same as createAxlyClient but strips toastHandler for Node.js compatibility.

createAxlyNodeClient<ConfigMap>(config: AxlyConfig | ConfigMap): AxlyClient

Client Methods

request<T, D>(options, stateUpdater?)

Make an HTTP request.

const response = await client.request<User>({
  method: 'GET',
  url: '/users/1'
});

RequestOptions

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<T>(url, formData, opts?)

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}%`)
});

UploadOptions

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

Token & Header Methods

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 header

Cache Invalidation

client.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 predicate

When 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.

Other Methods

client.cancelRequest(abortController); // Cancel a specific request
client.destroy(); // Cleanup all instances, caches, and timers
client.on('destroy', () => {}); // Listen to events

useAxly

General-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)

useAxlyQuery

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)
});

useAxlyQuery Options

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

useAxlyQuery Returns

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

useAxlyMutation

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' }
});

useAxlyMutation Options

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

useAxlyMutation Returns

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

πŸ’‘ Usage Examples

Basic Requests

// 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' }
});

Authentication & Token Management

// 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';
  }
});

Request Deduplication

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 })
]);

Response Caching

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
});

Stale-While-Revalidate

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.

Pattern-Based Cache Invalidation

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();

Progress Tracking

const { request, uploadProgress, downloadProgress } = useAxly(client);

await request({
  method: 'GET',
  url: '/large-file',
  responseType: 'blob',
  onDownloadProgress: (percent) => console.log(`Download: ${percent}%`)
});

File Upload

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);
};

Request Cancellation

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>;

Retry Logic

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)

Custom retry predicate (shouldRetry)

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.

Interaction with 401 token refresh

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.

Custom Auth Scheme

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.

Toast Notifications

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!'
});

Error Handling

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);
  }
}

Multiple API Configurations

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'
});

Interceptors

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;
    }
  ]
});

Node.js Usage

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' });

πŸ”¬ Advanced Features

Automatic Token Refresh

When multiToken: true and a request returns 401, Axly automatically:

  1. Calls your refreshEndpoint with the current refresh token
  2. Updates access and refresh tokens (via tokenCallbacks or in-memory)
  3. Retries the failed request with the new access token
  4. 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')
  }
});

Token Callbacks

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)
  }
});

Event Emitter

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();

TypeScript Support

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[] | undefined

Additional 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;
};

Error Classes

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 AxiosError

βœ… Best Practices

Create 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.


🀝 Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

  1. Fork the repository
  2. Create your feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -m 'Add my feature'
  4. Push to the branch: git push origin feature/my-feature
  5. Open a pull request

πŸ“„ License

MIT Β© Harshal Katakiya

About

Axly is a powerful and flexible HTTP client library built on top of Axios. It provides a streamlined interface for making API requests with additional features.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors