modern fetchwise

September 11, 2026 · View on GitHub

A small, type-safe HTTP client for TypeScript and JavaScript. Built on the Fetch API.

This package is not affiliated with the existing npm package fetchwise. Install this one as @poluru-labs/fetchwise.

Works in any JS environment — browsers, Node, Deno, Bun, React Native, and frameworks like React, Vue, Angular, Lit, Next.js, and Nuxt.

Automatic retries · Response validation · Interceptors · Generated API types · Multiple API hosts

import { createClient } from "@poluru-labs/fetchwise";

const api = createClient({
  baseURL: {
    default: "https://api.shop.com",
    auth: "https://auth.shop.com",
  },
  retry: 3,
});

const users = await api.get("/users");
const session = await api.get("/session", { baseURL: "auth" });

Install

npm install @poluru-labs/fetchwise
pnpm add @poluru-labs/fetchwise
yarn add @poluru-labs/fetchwise
bun add @poluru-labs/fetchwise

The unscoped name fetchwise is already taken on npm, so this package is published as @poluru-labs/fetchwise.

Works everywhere

EnvironmentHow to import
ESM / TypeScriptimport { createClient } from "@poluru-labs/fetchwise"
CommonJSconst { createClient } = require("@poluru-labs/fetchwise")
Default importimport fetchwise from "@poluru-labs/fetchwise"
Browser <script>fetchwise.createClient(...) via unpkg / jsDelivr
React, Vue, Angular, LitSame ESM import — no adapter
Next.js, Nuxt, SvelteKit, RemixSame import on server and client
Deno / Bun / WorkersSame ESM import

The core library has no Node-only APIs. It uses globalThis.fetch, so it runs anywhere Fetch exists.

<script src="https://unpkg.com/@poluru-labs/fetchwise"></script>
<script>
  const api = fetchwise.createClient({ baseURL: "https://api.shop.com" });
  api.get("/users").then(console.log);
</script>

Examples by stack: examples/ — TypeScript, JavaScript, React, Vue, Angular, and Lit.

Quick start

import { createClient } from "@poluru-labs/fetchwise";

type User = { id: number; name: string; email: string };

const api = createClient({
  baseURL: "https://api.shop.com",
  headers: { Accept: "application/json" },
  credentials: "include",
  timeout: 8_000,
  retry: { attempts: 3, delay: 300 },
});

const users = await api.get<User[]>("/users");
const user = await api.get<User>("/users/:id", { params: { id: 1 } });
const created = await api.post<User>("/users", {
  name: "Poluru Ada",
  email: "poluru.ada@shop.com",
});

Plain JavaScript is the same API. See examples/javascript.

Features

Automatic retries

Failed network calls, timeouts, and selected HTTP statuses are retried with backoff.

const api = createClient({
  baseURL: "https://api.example.com",
  retry: {
    attempts: 3,
    delay: 300,
    backoff: "exponential",
    retryOn: [408, 429, 500, 502, 503, 504],
  },
});

Pass retry: 3 for defaults, or retry: false to turn retries off.

Multiple API hosts

baseURL can be one host or a map of names. Pick a host per request, pass a full URL, or extend() a client for one API.

const api = createClient({
  baseURL: {
    default: "https://api.shop.com",
    auth: "https://auth.shop.com",
    payments: "https://payments.shop.com",
  },
});

await api.get("/users");
await api.get("/session", { baseURL: "auth" });
await api.get("/charges", { baseURL: "payments" });
await api.get("/health", { baseURL: "https://status.shop.com" });
await api.get("https://edge.shop.com/ping");

const payments = api.extend({ baseURL: { default: "https://payments.shop.com" } });
await payments.post("/charges", { amount: 2500 });

Response validation

Pass any schema with a parse() method. Zod, Valibot, ArkType, or a tiny custom object all work.

const UserSchema = {
  parse(data: unknown): User {
    return data as User;
  },
};

const user = await api.get("/users/1", { schema: UserSchema });

Invalid payloads throw ValidationError.

Request and response interceptors

api.interceptors.request.use((request) => {
  request.headers.set("Authorization", `Bearer ${token}`);
  return request;
});

api.interceptors.response.use((response) => {
  console.log(response.status, response.url);
  return response;
});

api.interceptors.error.use((error) => {
  if (error.status === 401) logout();
  return error;
});

Generated API types

Describe routes once. fetchwise infers params, bodies, and responses.

interface API {
  "GET /users": User[];
  "GET /users/:id": User;
  "POST /users": { body: CreateUser; response: User };
}

const api = createClient<API>({ baseURL: "https://api.shop.com" });

const users = await api.get("/users");
const user = await api.get("/users/:id", { params: { id: 1 } });
const created = await api.post("/users", { name: "Poluru Ada", email: "poluru.ada@shop.com" });

Or generate that interface from JSON:

npx @poluru-labs/fetchwise generate api.spec.json -o api.types.ts
{
  "name": "API",
  "models": {
    "User": "{ id: number; name: string; email: string }"
  },
  "routes": [
    { "method": "GET", "path": "/users", "response": "User[]" },
    { "method": "POST", "path": "/users", "body": "CreateUser", "response": "User" }
  ]
}

Named methods are available too:

import { defineApi } from "@poluru-labs/fetchwise";

const users = defineApi({
  client: api,
  routes: {
    getUser: { method: "GET", path: "/users/:id" },
    createUser: { method: "POST", path: "/users" },
  },
});

await users.getUser({ params: { id: 1 } });

Examples

Organized by stack in examples/:

FolderPattern
typescriptGenerics, retries, generated types
javascriptESM, CommonJS, browser script
reactShared client + useUsers hook
vueComposable + SFCs
angularApiService + signals
litLitElement custom elements
npm install
npm run example examples/typescript/basic.ts

API

createClient(options)

OptionTypeDefaultDescription
baseURLstring | { default?: string; [name: string]: string }Default host, or named hosts for multiple APIs
headersHeadersInitDefault headers
credentialsRequestCredentialsBrowser CORS cookies (include, same-origin)
timeoutnumberRequest timeout in ms
retrynumber | RetryOptions | falsefalseRetry policy
fetchtypeof fetchglobalThis.fetchCustom fetch implementation

fetchwise(options) and the default export are aliases of createClient(options).

Methods

api.get(url, config?)
api.post(url, body?, config?)
api.put(url, body?, config?)
api.patch(url, body?, config?)
api.delete(url, config?)
api.request(url, config?)   // returns parsed data
api.send(url, config?)      // returns { data, status, headers, raw }
api.extend(options)         // copy the client with new defaults

Request config

{
  baseURL, // named host or full URL for this request
  headers, query, params, body,
  timeout, retry, schema, signal,
  parseAs: "json" | "text" | "blob" | "auto" | "raw",
  fetchOptions, // extra Fetch init
}

Path tokens like /users/:id are filled from params.

Errors

ClassWhen
HTTPErrorResponse status is not 2xx
TimeoutErrorThe request exceeded timeout
ValidationErrorschema.parse() failed
FetchwiseErrorNetwork, abort, or other failures
import { HTTPError } from "@poluru-labs/fetchwise";

try {
  await api.get("/missing");
} catch (error) {
  if (error instanceof HTTPError || error?.name === "HTTPError") {
    console.log(error.status, error.data);
  }
}

Use error.name when multiple copies of the package are bundled.

License

MIT