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

@ma-dev/api-client

v0.1.5

Published

Shared HTTP client and token store for frontend projects.

Downloads

543

Readme

@ma-dev/api-client

Shared HTTP client infrastructure for frontend projects.

What's inside

| Export | Description | | -------------------------- | ------------------------------------------------------------------- | | createHttpClient(config) | Factory that returns a typed fetch wrapper with auto-auth injection | | ApiError<T> | Generic structured error class thrown on non-2xx responses | | tokenStore | Imperative singleton for storing the current bearer token | | ApiResponse<T> | Generic response envelope matching the API contract | | HttpClient | Inferred type of the object returned by createHttpClient | | HttpClientConfig | Config interface for createHttpClient | | RequestOptions | Per-request options (headers, params, signal, responseType) | | RetryConfig | Retry strategy configuration | | TokenRefreshConfig | Configuration for automatic token refresh | | QueryParams | Query parameter map type | | RequestInterceptor | Callback type for intercepting requests before dispatch | | ResponseInterceptor | Callback type for response interceptors | | ErrorInterceptor | Callback type for error interceptors |

Installation

# npm
npm install @ma-dev/api-client

# yarn
yarn add @ma-dev/api-client

# pnpm
pnpm add @ma-dev/api-client

# bun
bun add @ma-dev/api-client

Quick start

1. Create your project's HTTP client singleton

// src/lib/client.ts
import { createHttpClient, tokenStore } from "@ma-dev/api-client";

export const httpClient = createHttpClient({
  baseUrl: import.meta.env.VITE_API_URL,
  getToken: tokenStore.getToken,
  timeoutMs: 10_000,
  retry: { attempts: 2 },
});

2. Sync the token store from your auth state

// After login
tokenStore.setToken(authData.token);

// After logout
tokenStore.setToken(null);

3. Create domain services

// src/services/account.service.ts
import { httpClient } from "../lib/client";
import type { ApiResponse } from "@ma-dev/api-client";

interface LoginData {
  token: string;
  userId: string;
  roles: string[];
}
type LoginResponse = ApiResponse<LoginData>;

export const accountService = {
  login: (username: string, password: string) =>
    httpClient.post<LoginResponse>("/account/login", { username, password }),
};

4. Handle errors with Typed Body

import { ApiError } from "@ma-dev/api-client";

interface ValidationErrors {
  code: string;
  errors: string[];
}

try {
  await accountService.login(username, password);
} catch (err) {
  if (err instanceof ApiError) {
    // Cast/infer error body structure
    const errorBody = err.body as ValidationErrors;
    console.error(`HTTP ${err.status}: ${err.message}`, errorBody.errors);
  }
}

Features

Query parameters

Pass a params object in RequestOptions instead of building query strings manually. Arrays are serialised as repeated keys, and Date objects are automatically serialized to ISO 8601 strings.

// GET /users?page=2&role=admin&role=editor&created=2026-06-20T18%3A00%3A00.000Z
const users = await httpClient.get<UserListResponse>("/users", {
  params: {
    page: 2,
    role: ["admin", "editor"],
    created: new Date("2026-06-20T18:00:00.000Z")
  },
});

null and undefined values are automatically omitted.

Request timeouts

Set a global deadline for every request:

const httpClient = createHttpClient({
  baseUrl: "...",
  timeoutMs: 8_000, // 8 s global timeout
});

Or override per-request using a standard AbortSignal:

const result = await httpClient.get("/slow-endpoint", {
  signal: AbortSignal.timeout(3_000),
});

Retry with exponential back-off

const httpClient = createHttpClient({
  baseUrl: "...",
  retry: {
    attempts: 3, // up to 3 retries (4 total attempts)
    baseDelayMs: 300, // 300 ms → 600 ms → 1 200 ms
    retryOn: [429, 500, 502, 503, 504], // default if omitted
  },
});

Retries are never triggered for AbortError (user/timeout cancellations).

Automatic Token Refresh (Self-Healing)

You can configure automatic token refresh handling on 401 Unauthorized responses. The refresh handler is deduplicated: if multiple concurrent requests encounter a 401, only one refresh operation is executed, and all pending requests wait for its outcome before retrying.

const httpClient = createHttpClient({
  baseUrl: "...",
  getToken: () => localStorage.getItem("access_token"),
  tokenRefresh: {
    refresh: async () => {
      const response = await fetch("https://api.example.com/auth/refresh", {
        method: "POST",
        body: JSON.stringify({ refresh_token: localStorage.getItem("refresh_token") }),
        headers: { "Content-Type": "application/json" }
      });
      if (!response.ok) throw new Error("Failed to refresh token");
      const { access_token } = await response.json();
      localStorage.setItem("access_token", access_token);
      return access_token;
    },
    statusCodes: [401], // Defaults to [401]
  }
});

Interceptors

Request interceptors — called before a request is sent, allowing you to dynamically inspect and modify method, path, headers, or request options:

const httpClient = createHttpClient({
  baseUrl: "...",
  requestInterceptors: [
    ({ headers, options }) => {
      headers.set("X-Correlation-Id", crypto.randomUUID());
    },
  ],
});

Response interceptors — called after every response, useful for logging or analytics:

const httpClient = createHttpClient({
  baseUrl: "...",
  responseInterceptors: [
    (res, req) => {
      console.log(`[${req.method}] ${req.path} → ${res.status}`);
    },
  ],
});

Error interceptors — called before ApiError is thrown. Return true to suppress the error:

import { useNavigate } from "react-router-dom";

const httpClient = createHttpClient({
  baseUrl: "...",
  errorInterceptors: [
    (error) => {
      if (error.status === 401) {
        tokenStore.setToken(null);
        window.location.href = "/login";
        return true; // suppress the throw
      }
    },
  ],
});

Binary and Custom Response Types

Choose how response bodies are parsed (defaults to automatic detection of JSON/Text):

// Blob
const avatarBlob = await httpClient.get<Blob>("/profile/avatar", {
  responseType: "blob",
});

// ArrayBuffer
const buffer = await httpClient.get<ArrayBuffer>("/file", {
  responseType: "arraybuffer",
});

File uploads (multipart)

Supports uploading FormData with automatic Content-Type boundary detection. You can also specify custom methods (e.g., POST, PUT, PATCH):

const form = new FormData();
form.append("avatar", fileInput.files[0]);

const result = await httpClient.multipart<UploadResponse>(
  "/profile/avatar",
  form,
  { method: "PUT" } // Optional, defaults to POST
);

Custom fetch Implementation

Useful for SSR environments (e.g. Next.js), mock environments, or proxy routing:

const httpClient = createHttpClient({
  baseUrl: "...",
  fetch: customFetchWrapper, // Custom fetch injection
});

API reference

createHttpClient(config)

| Option | Type | Description | | ---------------------- | ------------------------ | ---------------------------------------------------------- | | baseUrl | string | Prepended to every path. No trailing slash. | | getToken | TokenGetter | Returns bearer token. Support async Promise<string \| null> or sync string \| null. | | defaultHeaders | Record<string, string> | Static headers merged into every request. | | timeoutMs | number | Global request deadline in ms. Uses AbortSignal.timeout. | | retry | RetryConfig | Retry strategy. Default: no retries. | | tokenRefresh | TokenRefreshConfig | Deduplicated token refresh configuration. | | requestInterceptors | RequestInterceptor[] | Called before request is dispatched. | | responseInterceptors | ResponseInterceptor[] | Called after every response. | | errorInterceptors | ErrorInterceptor[] | Called before an ApiError is thrown. | | fetch | typeof fetch | Custom fetch implementation override. |

RequestOptions

| Option | Type | Description | | -------------- | ------------- | ------------------------------------------------ | | headers | HeadersInit | Extra headers for this request only. | | params | QueryParams | Query-string parameters. Arrays → repeated keys. | | signal | AbortSignal | Cancellation / per-request timeout. | | responseType | "json" \| "text" \| "blob" \| "arraybuffer" | Forces response parsing method. |

RetryConfig

| Option | Type | Default | Description | | ------------- | ---------- | -------------------------------- | ------------------------------------------ | | attempts | number | — | Max retries after the initial failure. | | baseDelayMs | number | 300 | Base delay in ms for exponential back-off. | | retryOn | number[] | [408, 429, 500, 502, 503, 504] | Status codes that trigger a retry. |


Releasing and Changelog

This project uses standard-version to automatically generate CHANGELOG.md based on commit messages.

When you are ready to release a new version, simply run:

bun run release

This will automatically:

  1. Bump the version in package.json.
  2. Generate/update CHANGELOG.md with all the new commits since the last release.
  3. Commit the changes and create a Git tag for the new version.