npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zerotal/client

v1.7.5

Published

Typed HTTP client helpers for Zerotal applications.

Readme

@zerotal/client

Type-safe HTTP API client bound to a route map, with a built-in circuit breaker.

A fetch-based HTTP client that infers request bodies, path params, query params, and response shapes directly from a TypeScript route map — no casting. Includes request/response interceptors, 401 token-refresh handling, and a circuit breaker that fails fast when an upstream service is struggling.

@zerotal/client vs core's Http facade — they solve different problems, despite both speaking HTTP:

  • Use @zerotal/client (ApiClient) for the frontend/SPA consuming your own typed API — it's route-map-typed and ships a familiar realtime Socket.
  • Use core's Http facade (@zerotal/core) for server-to-server outgoing requests — fluent, with test fakes.

Part of the Zerotal framework. Requires Bun ≥ 1.3.14.

Installation

bun add @zerotal/client

Setup

Register the provider in bootstrap/providers.ts:

import { ClientProvider } from "@zerotal/client";

Usage

Define your API surface as a route map, then create a client bound to it:

// api-types.ts
export interface Routes {
  "GET /api/users": {
    query: { page?: number; search?: string };
    response: { data: UserResource[]; total: number };
  };
  "GET /api/users/{id}": { params: { id: number }; response: UserResource };
  "POST /api/users": { body: { name: string; email: string }; response: UserResource };
}
import { createApiClient } from "@zerotal/client";
import type { Routes } from "./api-types.ts";

export const api = createApiClient<Routes>({
  baseUrl: "https://api.example.com",
  headers: { Accept: "application/json" },
});

Request methods are fully typed against the route map:

const user = await api.get("/api/users/{id}", { id: 42 }); // -> UserResource
const list = await api.get("/api/users", undefined, { query: { page: 2, search: "alice" } });
const created = await api.post("/api/users", { name: "Alice", email: "[email protected]" });

Attach interceptors and 401 refresh handling at construction:

const api = createApiClient<Routes>({
  baseUrl: "https://api.example.com",
  onRequest: async (config) => ({
    ...config,
    headers: { ...config.headers, Authorization: `Bearer ${await tokenStore.get()}` },
  }),
  onUnauthorized: async (err, retry) => {
    const newToken = await authStore.refresh();
    return retry({ Authorization: `Bearer ${newToken}` }); // limited to one attempt
  },
});

Add a circuit breaker to fail fast against an unhealthy upstream:

import { CircuitBreaker, CircuitBreakerOpenError } from "@zerotal/client";

const api = createApiClient<Routes>({
  baseUrl: "https://api.example.com",
  circuitBreaker: { threshold: 5, resetTimeout: 30_000 },
});

// CircuitBreaker also wraps any async operation directly:
const breaker = new CircuitBreaker({ threshold: 3, resetTimeout: 15_000 });
const rates = await breaker.call(() =>
  fetch("https://pricing.internal/rates").then((r) => r.json()),
);

Non-2xx responses throw ApiClientError (with status, statusText, body); a 422 throws a typed ValidationError (.errors, .has(), .first()); an open circuit throws CircuitBreakerOpenError.

The client also handles auth & CSRF (token/setToken, withCredentials, XSRF-TOKENX-XSRF-TOKEN), timeouts & retries (timeout, retry with backoff + Retry-After), file uploads/downloads (FormData/Blob bodies, responseType: "blob"), nested query serialization (ids[]=, filter[x]=), and a per-request meta callback for response headers. See the docs.

Realtime: Socket

A small, dependency-free WebSocket client for @zerotal/broadcasting's native ws / redis drivers, with a familiar realtime-client API (and a drop-in for window.Echo). No external realtime client required.

import { Socket } from "@zerotal/client";

const socket = new Socket(); // ws(s)://<host>/app/ws

socket.channel("posts").listen("PostPublished", (e) => render(e));
socket.private(`orders.${id}`).listen("OrderUpdated", (e) => update(e.order));
socket
  .presence(`chat.${roomId}`)
  .here((members) => setOnline(members))
  .joining((m) => addOnline(m))
  .leaving((m) => removeOnline(m))
  .listen("Message", (e) => append(e));

Private/presence channels are authorized with a per-subscription HMAC signature (Pusher-style): the client fetches it from authEndpoint (default /broadcasting/auth, signed server-side with APP_KEY) and re-fetches on reconnect. Pass auth.headers for CSRF, or authEndpoint: false for connection-level auth. Auto-reconnects and re-subscribes; observe state with socket.on("connected"|"disconnected"|…, cb); use socket.socketId() as the X-Socket-ID header so server toOthers() broadcasts skip this client. See Broadcasting › Client-side.

Exports

  • createApiClient<Routes>(config) — factory returning a type-safe ApiClient.
  • ApiClient — the client class (get, post, put, patch, delete, setToken).
  • ApiClientError — thrown on non-2xx responses (status, statusText, body, retryAfterMs).
  • ValidationError — thrown on 422; exposes .errors, .has(), .first(), .fields(), .validationMessage.
  • CircuitBreaker — standalone breaker with .call(), .state, .failures, .reset().
  • CircuitBreakerOpenError — thrown immediately while the circuit is open.
  • Socket — realtime broadcasting client (channel, private, presence/join, leave, socketId, on).
  • Channel / PresenceChannel — channel objects (listen, stopListening, subscribed, error; presence adds here/joining/leaving).
  • ClientProvider / Client — service provider and facade.
  • ClientConfig — config factory.
  • Types: ApiRouteMap, RouteShape, HttpMethod, PathParams, ParamRecord, PathsFor, ResponseOf, BodyOf, QueryOf, ApiClientConfig, RequestConfig, RequestInterceptor, ResponseContext, ResponseInterceptor, ResponseMeta, RequestOptions, GetOptions, MutationOptions, TokenSource, RetryOptions, CircuitBreakerOptions, CircuitState, ClientConfigShape, SocketOptions, SocketState, SocketLike, PresenceMember.

Documentation