A TypeScript-first API client focused on secure browser auth: coalesced token refresh, Web Worker token isolation, and cross-tab session sync. Zero runtime dependencies, works in every JS runtime.
See the codeA TypeScript-first API client focused on secure browser auth: coalesced token refresh, Web Worker token isolation, and cross-tab session sync. Zero runtime dependencies, works in every JS runtime.
Documentation · Live demo · Wiki · Changelog
npm install @mrzr/api-client
import { createClient } from "@mrzr/api-client";
export const api = createClient({ baseUrl: "https://api.example.com" });
// Tokens are captured, stored, refreshed and rotated across tabs for you.
await api.login({ email, password });
const { data } = await api.get<User[]>("/users");
Use it if any of these are real problems for you:
Use something else if not. For a small fetch wrapper with built-in retry, use ky. For the widest legacy support and ecosystem, use axios. Neither focuses on browser auth.
| axios | ky | @mrzr/api-client | |
|---|---|---|---|
| Zero runtime dependencies | ✗ | ✓ | ✓ |
| Built on | XHR / node:http | fetch | fetch |
| Retry with backoff | via axios-retry | ✓ built in | ✗ (not yet) |
| Interceptors / hooks | ✓ global | ✓ global | ✓ global, via plugins |
| Coalesced token refresh | build it yourself | build it yourself | ✓ built in |
| Web Worker token isolation | ✗ | ✗ | ✓ |
| Cross-tab auth sync | ✗ | ✗ | ✓ |
| httpOnly cookie session restore | ✗ | ✗ | ✓ |
| Cancel by URL pattern / scope | ✗ | ✗ | ✓ |
| CSRF double-submit | partial | ✗ | ✓ |
An honest note on that table:
takeLatest, and shipping it half-right would be worse than not shipping it.Worker is missingBroadcastChannel, and tabs take turns refreshing (Web Locks), so a rotating refresh token is never spent twicerestoreSession(), which answers the "am I logged in?" question that cookies make unanswerable from JSFormData, File, Blob, typed arrays and streams, with refresh handled mid-uploadservices for several APIs on one session (guide)getSocketToken(url) hands a socket a server-issued ticket without exposing the access token; getAccessToken() is there behind exposeTokens: true (guide)<script>, Node 20+, Deno, Bun, Cloudflare WorkersIt sits under TanStack Query, SWR or Vue Query — it doesn't replace them.
useQuery({
queryKey: ["users"],
queryFn: ({ signal }) => api.get<User[]>("/users", { signal }).then((r) => r.data),
});
Failures reject with a typed ApiError, which is what Query and SWR need to mark a request failed. Cancellation resolves instead, flagged with canceled: true, so a route change never looks like an error.
// lib/api.ts
import { createClient } from "@mrzr/api-client";
export const api = createClient({
baseUrl: "https://api.example.com",
});
import { api } from "./lib/api";
import { ApiError } from "@mrzr/api-client";
try {
const { data } = await api.get<User[]>("/users");
console.log(data);
} catch (e) {
if (e instanceof ApiError) console.error(e.statusCode, e.message);
}
Worker isolation, token refresh and tab sync are on by default, and turn themselves off where the runtime doesn't support them.
api-client.mrzr.ir: short guides for every feature, and a live demo that runs the package in your browser.
For every detail, edge case and recipe, see the Wiki (Security model, Troubleshooting, Migration guide, FAQ).
Upgrading from 2.x? 3.0.0 has breaking changes — see the changelog.
Any runtime with fetch and AbortController: all modern browsers, Node 20+, Deno, Bun, Cloudflare Workers.
MIT
TypeScript
54.2%
JavaScript
45.4%
A TypeScript-first API client focused on secure browser auth: coalesced token refresh, Web Worker token isolation, and cross-tab session sync. Zero runtime dependencies, works in every JS runtime.
See the codeA TypeScript-first API client focused on secure browser auth: coalesced token refresh, Web Worker token isolation, and cross-tab session sync. Zero runtime dependencies, works in every JS runtime.
Documentation · Live demo · Wiki · Changelog
npm install @mrzr/api-client
import { createClient } from "@mrzr/api-client";
export const api = createClient({ baseUrl: "https://api.example.com" });
// Tokens are captured, stored, refreshed and rotated across tabs for you.
await api.login({ email, password });
const { data } = await api.get<User[]>("/users");
Use it if any of these are real problems for you:
Use something else if not. For a small fetch wrapper with built-in retry, use ky. For the widest legacy support and ecosystem, use axios. Neither focuses on browser auth.
| axios | ky | @mrzr/api-client | |
|---|---|---|---|
| Zero runtime dependencies | ✗ | ✓ | ✓ |
| Built on | XHR / node:http | fetch | fetch |
| Retry with backoff | via axios-retry | ✓ built in | ✗ (not yet) |
| Interceptors / hooks | ✓ global | ✓ global | ✓ global, via plugins |
| Coalesced token refresh | build it yourself | build it yourself | ✓ built in |
| Web Worker token isolation | ✗ | ✗ | ✓ |
| Cross-tab auth sync | ✗ | ✗ | ✓ |
| httpOnly cookie session restore | ✗ | ✗ | ✓ |
| Cancel by URL pattern / scope | ✗ | ✗ | ✓ |
| CSRF double-submit | partial | ✗ | ✓ |
An honest note on that table:
takeLatest, and shipping it half-right would be worse than not shipping it.Worker is missingBroadcastChannel, and tabs take turns refreshing (Web Locks), so a rotating refresh token is never spent twicerestoreSession(), which answers the "am I logged in?" question that cookies make unanswerable from JSFormData, File, Blob, typed arrays and streams, with refresh handled mid-uploadservices for several APIs on one session (guide)getSocketToken(url) hands a socket a server-issued ticket without exposing the access token; getAccessToken() is there behind exposeTokens: true (guide)<script>, Node 20+, Deno, Bun, Cloudflare WorkersIt sits under TanStack Query, SWR or Vue Query — it doesn't replace them.
useQuery({
queryKey: ["users"],
queryFn: ({ signal }) => api.get<User[]>("/users", { signal }).then((r) => r.data),
});
Failures reject with a typed ApiError, which is what Query and SWR need to mark a request failed. Cancellation resolves instead, flagged with canceled: true, so a route change never looks like an error.
// lib/api.ts
import { createClient } from "@mrzr/api-client";
export const api = createClient({
baseUrl: "https://api.example.com",
});
import { api } from "./lib/api";
import { ApiError } from "@mrzr/api-client";
try {
const { data } = await api.get<User[]>("/users");
console.log(data);
} catch (e) {
if (e instanceof ApiError) console.error(e.statusCode, e.message);
}
Worker isolation, token refresh and tab sync are on by default, and turn themselves off where the runtime doesn't support them.
api-client.mrzr.ir: short guides for every feature, and a live demo that runs the package in your browser.
For every detail, edge case and recipe, see the Wiki (Security model, Troubleshooting, Migration guide, FAQ).
Upgrading from 2.x? 3.0.0 has breaking changes — see the changelog.
Any runtime with fetch and AbortController: all modern browsers, Node 20+, Deno, Bun, Cloudflare Workers.
MIT
TypeScript
54.2%
JavaScript
45.4%