glide-fetch
v0.2.0
Published
Type-safe axios wrapper with auth refresh, retries, and request deduplication.
Readme
glide-fetch
A thin, type-safe wrapper around axios that eliminates boilerplate around authentication, retries, and request deduplication. Axios is a peer dependency and is never bundled.
Install
pnpm add glide-fetch axiosBefore / After
Before — every team hand-rolls its own axios instance, and the auth-refresh race condition below is a real bug that has shipped more than once: five components mount at once, each request 401s, and five separate /refresh calls fire concurrently.
// Before: manual axios setup, repeated in every project
const api = axios.create({ baseURL: '/api' });
api.interceptors.request.use(async (config) => {
const token = await getAccessToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(undefined, async (error) => {
if (error.response?.status === 401) {
// BUG: if 5 requests 401 at once, this fires 5 times.
const newToken = await refreshToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return api.request(error.config);
}
// No retry logic, no dedupe, no timeout typing, no cancellation cleanup.
throw error;
});After — configure once, then call securedFetch like a typed HTTP client. Refresh concurrency, retries, dedupe, and timeouts are handled for you.
import { configureSecureFetch, securedFetch } from 'glide-fetch';
configureSecureFetch({
baseURL: '/api',
getAccessToken: () => getAccessToken(),
refreshToken: () => refreshAccessToken(),
onUnauthorized: () => router.push('/login'),
});
const { data } = await securedFetch.get<User>('/users/1');API
configureSecureFetch(options)
Call once at app bootstrap.
| Option | Default | Notes |
|---|---|---|
| baseURL | — | required |
| getAccessToken | — | called fresh on every request; return null to send unauthenticated |
| refreshToken | — | called at most once concurrently; not retried on failure |
| onUnauthorized | — | called when refreshToken rejects |
| onError | — | called for every error ultimately surfaced to callers |
| defaultTimeout | 30000 | ms |
| maxRetries | 3 | |
| retryDelay | 1000 * 2^n | exponential backoff |
| dedupeWindow | 500 | ms |
securedFetch.{get,post,put,patch,delete}<T>(url, [data], [config])
Returns Promise<{ data: T; status: number }>. All methods are generic — securedFetch.get<User>('/users/1') types data as User.
config accepts headers, params, timeout, skipAuth, skipDedupe, dedupeKey, signal.
Errors
Every rejected call throws a SecureFetchError (or its subclass TimeoutError):
try {
await securedFetch.get('/users/1');
} catch (error) {
if (error instanceof TimeoutError) { /* ... */ }
if (error instanceof SecureFetchError) {
console.log(error.status, error.isCanceled, error.response);
}
}Behavior you can rely on
- Token refresh is single-flight. N concurrent 401s trigger exactly one
refreshToken()call; all N requests wait on it and retry once it resolves. - GET deduplication is on by default (identical method + URL + sorted params + a small set of relevant headers, within a 500ms window). POST/PUT/PATCH/DELETE are never deduped.
- Retries only happen for network errors,
408, and5xx— never for other4xx, never for401(that's the refresh flow's job), never for canceled requests. - Cancellation via
AbortSignalremoves the request from the dedupe queue and skips retries.
Bundle size
Target: < 5kb gzipped (axios excluded, as a peer dependency). See /BUNDLE_SIZE.md at the repo root for methodology and current numbers.
