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" });npm install @poluru-labs/fetchwisepnpm add @poluru-labs/fetchwise
yarn add @poluru-labs/fetchwise
bun add @poluru-labs/fetchwiseThe unscoped name
fetchwiseis already taken on npm, so this package is published as@poluru-labs/fetchwise.
| Environment | How to import |
|---|---|
| ESM / TypeScript | import { createClient } from "@poluru-labs/fetchwise" |
| CommonJS | const { createClient } = require("@poluru-labs/fetchwise") |
| Default import | import fetchwise from "@poluru-labs/fetchwise" |
Browser <script> |
fetchwise.createClient(...) via unpkg / jsDelivr |
| React, Vue, Angular, Lit | Same ESM import — no adapter |
| Next.js, Nuxt, SvelteKit, Remix | Same import on server and client |
| Deno / Bun / Workers | Same 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.
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.
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.
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 });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.
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;
});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 } });Organized by stack in examples/:
| Folder | Pattern |
|---|---|
typescript |
Generics, retries, generated types |
javascript |
ESM, CommonJS, browser script |
react |
Shared client + useUsers hook |
vue |
Composable + SFCs |
angular |
ApiService + signals |
lit |
LitElement custom elements |
npm install
npm run example examples/typescript/basic.ts| Option | Type | Default | Description |
|---|---|---|---|
baseURL |
string | { default?: string; [name: string]: string } |
Default host, or named hosts for multiple APIs | |
headers |
HeadersInit |
Default headers | |
credentials |
RequestCredentials |
Browser CORS cookies (include, same-origin) |
|
timeout |
number |
Request timeout in ms | |
retry |
number | RetryOptions | false |
false |
Retry policy |
fetch |
typeof fetch |
globalThis.fetch |
Custom fetch implementation |
fetchwise(options) and the default export are aliases of createClient(options).
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{
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.
| Class | When |
|---|---|
HTTPError |
Response status is not 2xx |
TimeoutError |
The request exceeded timeout |
ValidationError |
schema.parse() failed |
FetchwiseError |
Network, 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.