axios-auth-client
v1.2.0
Published
A lightweight, framework-agnostic Axios wrapper with automatic JWT access token refresh (proactive + reactive), 401 retry logic, and custom refresh support.
Maintainers
Readme
axios-auth-client
A lightweight, framework-agnostic Axios wrapper with automatic JWT access token refresh (proactive + reactive), 401 retry logic, and custom refresh support — written in TypeScript.
Features
- Proactive token refresh — Detects when the JWT is about to expire (configurable threshold) and refreshes it before the request is sent
- Reactive 401 handling — Automatically retries failed requests after refreshing the token on a
401 Unauthorizedresponse - Queue / subscriber pattern — Concurrent requests during a token refresh are queued and replayed once the new token is available — no duplicate refresh calls
- Custom refresh function — Bring your own refresh logic via
customRefreshFnfor non-standard auth flows - Flexible token response path — Extract the new access token from any nested field in the refresh endpoint response using dot-notation (e.g.
"data.token") - Configurable token header — Use
Authorization,x-access-token, or any custom header - Public client mode — Skip auth entirely for unauthenticated routes via
isPublic: true - Full TypeScript support — Ships with
.d.tsdeclarations; all Axios types are re-exported - Zero runtime dependencies —
axiosis a peer dependency; the package itself has no extra runtime deps - Universal environment support — Works in browsers (uses
atob) and Node.js (manual base64 fallback) without any polyfills
Installation
npm install axios-auth-client axios
# or
yarn add axios-auth-client axiosPeer dependency:
axios ^1.6.0is required.
Quick Start
import { ApiClient } from "axios-auth-client";
const client = new ApiClient(
// Axios config (baseURL, timeout, etc.)
{ baseURL: "https://api.example.com" },
// Auth config
{
getAccessToken: () => localStorage.getItem("accessToken"),
getRefreshToken: () => localStorage.getItem("refreshToken"),
setAccessToken: (token) => localStorage.setItem("accessToken", token),
removeTokens: () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
},
refreshTokenEndpoint: "/auth/refresh",
onAuthFailure: () => {
// e.g. redirect to login
window.location.href = "/login";
},
},
);
// Use like a normal HTTP client
const user = await client.get<User>("/users/me");
const created = await client.post<Post>("/posts", { title: "Hello" });API Reference
new ApiClient(axiosConfig, authConfig, options?)
| Parameter | Type | Description |
| ------------- | --------------------- | ---------------------------------------------------------------- |
| axiosConfig | CreateAxiosDefaults | Standard Axios instance configuration (baseURL, headers, etc.) |
| authConfig | AuthConfig | Token management callbacks and refresh settings |
| options | ApiClientOptions | Optional — pass { isPublic: true } to skip auth entirely |
AuthConfig
| Property | Type | Required | Default | Description |
| ------------------------ | ---------------------------------------------- | -------- | ----------------- | ----------------------------------------------------------------------------------- |
| getAccessToken | () => string \| null | ✅ | — | Returns the current access token |
| getRefreshToken | () => string \| null | ✅ | — | Returns the current refresh token |
| setAccessToken | (token: string) => void | ✅ | — | Persists the new access token |
| removeTokens | () => void | ✅ | — | Clears both tokens (called on auth failure) |
| refreshTokenEndpoint | string | ✅ | — | Endpoint path for token refresh (e.g. "/auth/refresh") |
| onAuthFailure | () => void | ❌ | undefined | Called after refresh fails — use to redirect to login |
| tokenHeader | string | ❌ | "Authorization" | HTTP header used to send the access token |
| expiryThresholdSeconds | number | ❌ | 60 | Seconds before JWT expiry to trigger a proactive refresh |
| customRefreshFn | (instance: AxiosInstance) => Promise<string> | ❌ | undefined | Override the default refresh logic with your own |
| sendRefreshTokenInBody | boolean | ❌ | false | Send the refresh token in the request body instead of the header |
| tokenResponsePath | string | ❌ | "accessToken" | Dot-notation path to the access token in the refresh response (e.g. "data.token") |
HTTP Methods
All methods are generic and return Promise<T> (unwrapped response.data).
client.get<T>(url, params?, config?)
client.post<T, D>(url, data?, params?, config?)
client.put<T, D>(url, data?, params?, config?)
client.patch<T, D>(url, data?, params?, config?)
client.delete<T>(url, params?, config?)Utility Methods
// Update default headers on the underlying Axios instance
client.updateDefaultHeaders({ "x-tenant-id": "abc123" });
// Access the raw Axios instance for advanced use cases
const axiosInstance = client.getAxiosInstance();Advanced Usage
Custom Refresh Function
If your API has a non-standard token refresh flow, pass a customRefreshFn:
const client = new ApiClient(
{ baseURL: "https://api.example.com" },
{
// ...required callbacks...
refreshTokenEndpoint: "/auth/refresh", // still required to avoid interceptor loops
customRefreshFn: async (axiosInstance) => {
const res = await axiosInstance.post("/auth/custom-refresh", {
token: mySpecialToken,
});
return res.data.jwt; // return the new access token string
},
},
);Nested Token Response Path
If your refresh endpoint returns the token at a nested path:
{ "data": { "tokens": { "access": "eyJ..." } } }Set tokenResponsePath accordingly:
tokenResponsePath: "data.tokens.access";Public Client (No Auth)
For routes that don't need authentication:
const publicClient = new ApiClient(
{ baseURL: "https://api.example.com" },
authConfig,
{ isPublic: true },
);
const posts = await publicClient.get("/public/posts");Refresh Token Sent in Body
Some APIs expect the refresh token in the POST body:
sendRefreshTokenInBody: true;
// POSTs: { refreshToken: "<token>" }How It Works
Proactive Refresh (Pre-request)
On every outgoing request, the request interceptor decodes the JWT payload and checks the exp claim. If the token expires within expiryThresholdSeconds (default: 60s), a refresh is triggered before the request is dispatched. This prevents unnecessary 401s.
Reactive Refresh (Post-401)
If the server responds with a 401 Unauthorized, the response interceptor catches it (once per request, via an _retry flag), refreshes the token, and replays the original request with the new token.
Concurrent Request Queuing
If multiple requests fire simultaneously while a refresh is already in-progress, they are queued via a subscriber/promise pattern. All queued requests resume automatically once the single refresh resolves — ensuring only one refresh call is ever made at a time.
Request A ──► isRefreshing=true ──► refresh call ──► newToken ──► retry A
Request B ──► queued ────────────────────────────────────────► retry B
Request C ──► queued ────────────────────────────────────────► retry CTypeScript
Full type definitions are included. Commonly used Axios types are re-exported for convenience:
import type {
AuthConfig,
ApiClientOptions,
QueryParams,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
AxiosError,
} from "axios-auth-client";Requirements
- Node.js
>= 16 - TypeScript
>= 5.x(for consumers using TypeScript) axios ^1.6.0(peer dependency)
