vicks
v1.0.0
Published
A lightweight, zero-dependency wrapper for the fetch API with interceptors, retry, timeout, and more.
Downloads
15
Maintainers
Readme
Vicks - Fetch API with Superpowers
Vicks is a lightweight, zero-dependency wrapper built on the fetch API for browsers, Node.js, and Deno. It is fully compatible with the native fetch API while providing interceptors, retry, timeout, error handling, and more. Written in TypeScript with full type safety.
Features
- Zero Dependencies — no third-party libraries, tiny bundle size
- Interceptors — request, response, and error interceptors with chaining
- Timeout — built-in request timeout via
AbortSignal - Retry — automatic retry with configurable delay, status codes, and method safety
- Error Handling —
throwOnErrorandvalidateStatusfor automatic HTTP error throwing - Default Configurations — set headers, credentials, timeout, and more per client
- Multiple Clients — isolated instances with independent config and interceptors
- TypeScript — full type definitions,
HeadersInitsupport, typed responses - Fetch Compatible — all
fetchoptions (signal,credentials,mode,cache, etc.) pass through
Installation
npm install vicks
yarn add vicks
pnpm install vicksQuick Start
import { vicks } from "vicks";
// Simple GET
const response = await vicks.get("https://api.example.com/posts");
const posts = await response.json();
// With a configured client
const client = vicks.create({
baseUrl: "https://api.example.com",
headers: { "Authorization": "Bearer token" },
throwOnError: true,
timeout: 10000,
});
const users = await client.get("/users");Creating a Client
Create a client with default configurations. Each client has its own interceptors and defaults with no interference between instances.
import { vicks } from "vicks";
const client = vicks.create({
baseUrl: "https://jsonplaceholder.typicode.com",
headers: { "X-App-Name": "my-app" },
credentials: "include",
timeout: 5000,
});
const response = await client.get("/posts");
const posts = await response.json();Default Client
Vicks exports a pre-configured default client:
import { vicks } from "vicks";
const response = await vicks.get("https://jsonplaceholder.typicode.com/posts");You can modify the default client's configuration at runtime:
vicks.defaults.baseUrl = "https://jsonplaceholder.typicode.com";
vicks.defaults.headers = { "Authorization": "Bearer token" };
const response = await vicks.get("/posts");Making Requests
All methods return a native fetch Response object. You can pass any option supported by the fetch API alongside Vicks-specific options.
GET
const response = await client.get("/posts", {
params: { userId: 1 },
signal: abortController.signal,
});POST
The body is the second argument, config is the third:
const response = await client.post("/posts", { title: "foo", body: "bar", userId: 1 }, {
params: { draft: true },
});PUT
const response = await client.put("/posts/1", { title: "updated" });PATCH
const response = await client.patch("/posts/1", { title: "patched" });DELETE
const response = await client.delete("/posts/1");Automatic Content-Type
Vicks automatically sets Content-Type based on the body type:
| Body Type | Content-Type |
|---|---|
| Plain object | application/json (body is JSON.stringify'd) |
| string | text/plain |
| URLSearchParams | application/x-www-form-urlencoded;charset=UTF-8 |
| Blob | application/octet-stream |
| FormData | Set automatically by the browser |
You can override by passing headers:
await client.post("/data", myBody, {
headers: { "Content-Type": "text/xml" },
});Timeout
Set a timeout in milliseconds. The request will abort if it takes longer:
const client = vicks.create({
baseUrl: "https://api.example.com",
timeout: 5000, // 5 seconds
});
// Or per-request:
await client.get("/slow-endpoint", { timeout: 15000 });Timeout composes correctly with a user-provided signal — both can abort the request.
Retry
Automatic retry for transient failures:
const client = vicks.create({
baseUrl: "https://api.example.com",
retry: {
retries: 3,
retryDelay: 1000, // ms between retries
retryOn: [503, 502], // retry on these status codes
},
});Safety constraints
- Idempotent methods only by default: Only
GET,PUT, andDELETEare retried.POSTandPATCHare excluded unless explicitly added viaretryMethods. - Non-replayable bodies are never retried:
ReadableStreambodies skip retries regardless of config. - User aborts are never retried: Only timeout-related abort errors trigger retries.
// Explicitly allow POST retries (use with caution)
await client.post("/idempotent-endpoint", data, {
retry: { retries: 2, retryOn: [503], retryMethods: ["GET", "POST"] },
});Exponential backoff
const client = vicks.create({
retry: {
retries: 3,
retryDelay: (attempt) => Math.pow(2, attempt) * 1000, // 1s, 2s, 4s
},
});Error Handling
throwOnError
By default, Vicks behaves like fetch — non-2xx responses don't throw. Enable throwOnError to throw an HttpError on error status codes:
import { vicks, HttpError } from "vicks";
const client = vicks.create({
baseUrl: "https://api.example.com",
throwOnError: true,
});
try {
await client.get("/not-found");
} catch (error) {
if (error instanceof HttpError) {
console.log(error.status); // 404
console.log(error.response); // the Response object
}
}validateStatus
For custom status validation:
const client = vicks.create({
validateStatus: (status) => status < 500, // only throw on 5xx
});Both throwOnError and validateStatus run after response interceptors, so interceptors can still inspect and act on error responses (e.g., redirect on 401).
Interceptors
Vicks provides request, response, and error interceptors.
Request Interceptors
Modify the request config before it's sent. Interceptors receive the full config object. The return value is deep-merged with the current config. To remove a header, mutate the config directly.
client.interceptors.request.use((config) => {
// Add a header (via merge)
return { ...config, headers: { ...config.headers, "Authorization": getToken() } };
});
// Remove a header (via mutation)
client.interceptors.request.use((config) => {
const headers = config.headers as Record<string, string>;
delete headers["x-unwanted"];
return config;
});Request interceptors fail fast — if one throws, the request is aborted and the error flows to error interceptors.
Response Interceptors
Transform or inspect the response:
client.interceptors.response.use((response) => {
if (response.status === 401) {
window.location.href = "/login";
}
return response; // Must return a Response object
});Error Interceptors
Catch errors from any stage of the pipeline (network failures, timeouts, interceptor errors, HttpError from throwOnError):
client.interceptors.error.use((error) => {
if (error instanceof HttpError && error.status === 401) {
// Recover by returning a new Response
return refreshTokenAndRetry();
}
// Re-throw by returning the error
return error;
});If an error interceptor returns a Response, it's treated as a recovery — the response goes through the normal response interceptor and status validation pipeline before being returned to the caller.
Removing Interceptors
Each use() returns a removal function:
const remove = client.interceptors.request.use(attachToken);
// Later:
remove();Clearing All Interceptors
client.interceptors.clear(); // Clear all
client.interceptors.request.clear(); // Clear request only
client.interceptors.response.clear(); // Clear response only
client.interceptors.error.clear(); // Clear error onlyRequest Pipeline
The full request lifecycle:
request interceptors → fetch() → response interceptors → validateStatus/throwOnError → return
↓ (on error)
error interceptors → throw or recoverConfiguration Options
Options can be set as client defaults or per-request overrides.
| Option | Type | Description |
|---|---|---|
| baseUrl | string | Base URL prepended to relative endpoints. Handles trailing slashes. |
| headers | HeadersInit | Default headers. Accepts Record, Headers, or [key, value][] tuples. Merged case-insensitively. |
| params | Record / URLSearchParams / string | Query parameters. Multi-value keys are preserved. |
| timeout | number | Request timeout in milliseconds. |
| retry | RetryConfig | Retry configuration (see Retry). |
| throwOnError | boolean | Throw HttpError on non-2xx responses. Default: false. |
| validateStatus | (status: number) => boolean | Custom status validation function. |
| credentials | RequestCredentials | Fetch credentials option (omit, same-origin, include). |
| mode | RequestMode | Fetch mode (cors, no-cors, same-origin). |
| cache | RequestCache | Fetch cache option. |
| signal | AbortSignal | Abort signal (per-request only, not in defaults). |
| All fetch options | — | Any RequestInit property is passed through to fetch(). |
Migration from v0.x
Breaking changes in v1.0
baseUrlreplacesbaseURL:baseURLstill works but logs a deprecation warning. It will be removed in v2.- Headers are case-insensitive: Headers are normalized via the
HeadersAPI.Content-Typeandcontent-typeare the same header. Header keys in fetch calls will be lowercase. - Request interceptors fail fast: A throwing interceptor now aborts the request instead of silently continuing. This prevents sending misconfigured requests (e.g., missing auth).
- POST/PUT/PATCH method signatures:
post(endpoint, body, config)— body is the second argument, config is third. This hasn't changed, but previous README examples were incorrect.
License
MIT
