mohammadreza-zr/api-client

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.

TypeScript

0

58 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built a fetch client that handles auth tokens for you: one refresh for many 401s, tokens kept in a Web Worker, tabs stay in sync (r/reactjs)

Almost every app that logs users in ends up with the same hand-written refresh interceptor. It usually has the same bugs: 20 requests get a 401 at once and fire 20 refreshes, a rotating refresh token gets spent twice and the server logs the user out, or the token sits in `localStorage` where any…

1

Sep 28, 2026

README

@mrzr/api-client

npm types license

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.

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

Is this for you?

Use it if any of these are real problems for you:

  • Your access token expires and 20 concurrent requests each fire their own refresh
  • You want tokens off the main thread, where XSS can't read them
  • Logging out in one tab should log out the others
  • You use httpOnly cookies and can't tell on page load whether a session exists
  • You need refresh to work during a five-minute file upload

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.


How it compares

axiosky@mrzr/api-client
Zero runtime dependencies✗✓✓
Built onXHR / node:httpfetchfetch
Retry with backoffvia axios-retry✓ built in✗ (not yet)
Interceptors / hooks✓ global✓ global✓ global, via plugins
Coalesced token refreshbuild it yourselfbuild it yourself✓ built in
Web Worker token isolation✗✗✓
Cross-tab auth sync✗✗✓
httpOnly cookie session restore✗✗✓
Cancel by URL pattern / scope✗✗✓
CSRF double-submitpartial✗✓

An honest note on that table:

  • Retry. Not implemented. It has to interact correctly with refresh-and-retry, cancellation and takeLatest, and shipping it half-right would be worse than not shipping it.

What it actually does

  • Coalesced refresh — 50 simultaneous 401s trigger exactly one refresh call. A shared promise, not a polling loop
  • Web Worker isolation — requests run in a worker by default, so tokens never enter the main-thread heap. Self-disables on the server or where Worker is missing
  • Cross-tab sync — login, logout and refresh propagate over BroadcastChannel, and tabs take turns refreshing (Web Locks), so a rotating refresh token is never spent twice
  • httpOnly cookie mode — including restoreSession(), which answers the "am I logged in?" question that cookies make unanswerable from JS
  • Opt-in cancellation — cancel by URL pattern, scope or key on page change or modal close; real aborts, worker mode included
  • Real upload support — FormData, File, Blob, typed arrays and streams, with refresh handled mid-upload
  • CSRF double-submit — built in, for cookie auth
  • Plugins — optional add-ons that cost nothing until imported, like services for several APIs on one session (guide)
  • WebSockets / socket.io — getSocketToken(url) hands a socket a server-issued ticket without exposing the access token; getAccessToken() is there behind exposeTokens: true (guide)
  • One request engine — the worker and main thread run the same compiled code, so behaviour can't drift between modes
  • Runs anywhere — React, Vue, Svelte, Angular, Next.js, Nuxt, SvelteKit, plain <script>, Node 20+, Deno, Bun, Cloudflare Workers

Works with your data library

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


Quick start

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


📚 Documentation

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.


Requirements

Any runtime with fetch and AbortController: all modern browsers, Node 20+, Deno, Bun, Cloudflare Workers.

License

MIT

api-client
fetch
http-client
httponly
httponly-cookie
jwt
multi-tab
nextjs
nuxtjs
react
reactjs
rest
ssr
token-refresh
typescript
vue
vuejs
web-worker

mohammadreza-zr/api-client

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.

TypeScript

0

58 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built a fetch client that handles auth tokens for you: one refresh for many 401s, tokens kept in a Web Worker, tabs stay in sync (r/reactjs)

Almost every app that logs users in ends up with the same hand-written refresh interceptor. It usually has the same bugs: 20 requests get a 401 at once and fire 20 refreshes, a rotating refresh token gets spent twice and the server logs the user out, or the token sits in `localStorage` where any…

1

Sep 28, 2026

README

@mrzr/api-client

npm types license

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.

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

Is this for you?

Use it if any of these are real problems for you:

  • Your access token expires and 20 concurrent requests each fire their own refresh
  • You want tokens off the main thread, where XSS can't read them
  • Logging out in one tab should log out the others
  • You use httpOnly cookies and can't tell on page load whether a session exists
  • You need refresh to work during a five-minute file upload

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.


How it compares

axiosky@mrzr/api-client
Zero runtime dependencies✗✓✓
Built onXHR / node:httpfetchfetch
Retry with backoffvia axios-retry✓ built in✗ (not yet)
Interceptors / hooks✓ global✓ global✓ global, via plugins
Coalesced token refreshbuild it yourselfbuild it yourself✓ built in
Web Worker token isolation✗✗✓
Cross-tab auth sync✗✗✓
httpOnly cookie session restore✗✗✓
Cancel by URL pattern / scope✗✗✓
CSRF double-submitpartial✗✓

An honest note on that table:

  • Retry. Not implemented. It has to interact correctly with refresh-and-retry, cancellation and takeLatest, and shipping it half-right would be worse than not shipping it.

What it actually does

  • Coalesced refresh — 50 simultaneous 401s trigger exactly one refresh call. A shared promise, not a polling loop
  • Web Worker isolation — requests run in a worker by default, so tokens never enter the main-thread heap. Self-disables on the server or where Worker is missing
  • Cross-tab sync — login, logout and refresh propagate over BroadcastChannel, and tabs take turns refreshing (Web Locks), so a rotating refresh token is never spent twice
  • httpOnly cookie mode — including restoreSession(), which answers the "am I logged in?" question that cookies make unanswerable from JS
  • Opt-in cancellation — cancel by URL pattern, scope or key on page change or modal close; real aborts, worker mode included
  • Real upload support — FormData, File, Blob, typed arrays and streams, with refresh handled mid-upload
  • CSRF double-submit — built in, for cookie auth
  • Plugins — optional add-ons that cost nothing until imported, like services for several APIs on one session (guide)
  • WebSockets / socket.io — getSocketToken(url) hands a socket a server-issued ticket without exposing the access token; getAccessToken() is there behind exposeTokens: true (guide)
  • One request engine — the worker and main thread run the same compiled code, so behaviour can't drift between modes
  • Runs anywhere — React, Vue, Svelte, Angular, Next.js, Nuxt, SvelteKit, plain <script>, Node 20+, Deno, Bun, Cloudflare Workers

Works with your data library

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


Quick start

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


📚 Documentation

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.


Requirements

Any runtime with fetch and AbortController: all modern browsers, Node 20+, Deno, Bun, Cloudflare Workers.

License

MIT

api-client
fetch
http-client
httponly
httponly-cookie
jwt
multi-tab
nextjs
nuxtjs
react
reactjs
rest
ssr
token-refresh
typescript
vue
vuejs
web-worker

Languages

TypeScript

54.2%

JavaScript

45.4%