https://github.com/kinetexjs/kinetex
The universal HTTP client.
https://github.com/kinetexjs/kinetex
browser bun cloudflare deno fetch-api fetch-client http http-client https https-client nodefetch nodejs undici
Last synced: 4 days ago
JSON representation
The universal HTTP client.
- Host: GitHub
- URL: https://github.com/kinetexjs/kinetex
- Owner: kinetexjs
- License: mit
- Created: 2026-04-01T17:34:15.000Z (5 days ago)
- Default Branch: main
- Last Pushed: 2026-04-01T21:18:42.000Z (5 days ago)
- Last Synced: 2026-04-02T04:42:36.201Z (5 days ago)
- Topics: browser, bun, cloudflare, deno, fetch-api, fetch-client, http, http-client, https, https-client, nodefetch, nodejs, undici
- Language: JavaScript
- Homepage: https://kinetexjs.github.io/kinetex/
- Size: 171 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
README
> **The universal HTTP client for the modern JavaScript ecosystem.**
> Zero runtime dependencies. Every runtime. Every style.
[](https://www.npmjs.com/package/kinetex)
[](https://jsr.io/@kinetexjs/kinetex)
[](https://www.typescriptlang.org/)
[](https://codecov.io/gh/kinetexjs/kinetex)
[](https://npmjs.com/package/kinetex)
[](https://kinetexjs.github.io/kinetex)
---
## Why kinetex?
kinetex is a **batteries-included fetch wrapper** for modern JavaScript runtimes (Node 18+, browsers, Deno, Bun, Cloudflare Workers). It sits above `fetch` and optionally `undici`, adding the things you would otherwise wire up yourself:
- Built-in auth (Basic, Bearer, OAuth 2.0, AWS SigV4, Digest, API key)
- Stale-while-revalidate cache with custom store support
- Retry with exponential back-off
- Request deduplication
- SSE streaming with auto-reconnect
- GraphQL helper, HAR recording, cookie jar, proxy middleware
- A composable middleware pipeline
- End-to-end TypeScript generics
**What it is not:** a low-level HTTP client. It does not reimplement HTTP/2, connection pooling, or TLS — those come from `undici` (optional peer dep) or the platform's native `fetch`. If you need raw socket control or maximum throughput without any abstraction overhead, use undici directly.
See [`benchmarks/http-comparison.mjs`](benchmarks/http-comparison.mjs) for an honest performance comparison.
---
## ⚡ Performance Benchmarks
Kinetex is engineered for zero-cost abstraction. By leveraging a highly optimized middleware pipeline and the `undici` engine, it provides a "luxury" feature set (Auth, Retries, Cache, Hooks) with performance that rivals or beats native low-level clients.
### 📊 Comparison Table
*Measured on Node.js v22.22.1 (Small JSON payload, 500 measured + 50 warmup, Concurrency: 1)*
| Client | Req/s | Mean Latency | p99 Latency | Heap Δ (KB/req) |
| :--- | :---: | :---: | :---: | :---: |
| **Kinetex + Undici** ★ | **585** | **1.71 ms** | **2.73 ms** | **4.5 KB** |
| [Axios](https://github.com/axios/axios) | 570 | 1.75 ms | 3.71 ms | 16.6 KB |
| [Node-Fetch](https://github.com/node-fetch/node-fetch) | 518 | 1.93 ms | 3.92 ms | 14.5 KB |
| [Native Fetch](https://nodejs.org/api/globals.html#fetch) | 511 | 1.96 ms | 4.94 ms | 21.6 KB |
| **Kinetex (Standard)** ★ | **509** | **1.96 ms** | **4.86 ms** | **5.4 KB** |
| [Got](https://github.com/sindresorhus/got) | 508 | 1.97 ms | 2.73 ms | 18.5 KB |
| [Ky](https://github.com/sindresorhus/ky) | 485 | 2.06 ms | 2.61 ms | 23.2 KB |
| [Superagent](https://github.com/ladjs/superagent) | 386 | 2.59 ms | 4.42 ms | 22.9 KB |
---
### 🚀 Key Takeaways
#### 1. Zero-Cost Middleware Pipeline
The Kinetex pipeline adds only **~0.007ms** of overhead per request. You get advanced features like AWS SigV4, automatic retries, and request deduplication for essentially zero CPU tax. Once the V8 JIT compiler optimizes the pipeline, the overhead becomes practically immeasurable.
#### 2. Extreme Memory Efficiency
Kinetex uses **75% less memory** per request than native fetch and **70% less** than Axios. In high-traffic production environments, this significantly reduces **Garbage Collection (GC) pressure**, leading to lower CPU spikes and a smaller infrastructure footprint.
#### 3. Tail Latency Stability
With a p99 latency of **2.73ms**, Kinetex is more predictable under load than native fetch (4.94ms). This prevents the random "lag spikes" often seen in microservices when using standard promise-based wrappers.
#### 4. Green Computing
By reducing heap allocation by **15-18KB per request** compared to competitors, Kinetex allows your containers to handle more concurrent traffic with less RAM, directly lowering your cloud computing costs.
---
> **Note:** While `undici (direct)` and `node:http (raw)` are faster, they lack the high-level features (middleware, automatic JSON parsing, easy auth) that Kinetex provides. Kinetex aims to be the fastest **feature-complete** HTTP client for the Node.js ecosystem.
*Run the benchmarks yourself:*
```bash
node --expose-gc benchmarks/http-comparison.mjs
```
---
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Runtime Support](#runtime-support)
- [API Styles](#api-styles)
- [Async / Await](#async--await)
- [Fluent Chain](#fluent-chain)
- [Callback](#callback)
- [Instances & Configuration](#instances--configuration)
- [Request Config Reference](#request-config-reference)
- [Response Object](#response-object)
- [Authentication](#authentication)
- [Schema Validation](#schema-validation)
- [Caching](#caching)
- [Retry](#retry)
- [Interceptors](#interceptors)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Middleware Pipeline](#middleware-pipeline)
- [SSE Streaming](#sse-streaming)
- [GraphQL](#graphql)
- [File Upload & Progress](#file-upload--progress)
- [HAR Recording](#har-recording)
- [Cookie Jar](#cookie-jar)
- [Proxy & SOCKS5](#proxy--socks5)
- [Concurrency & Rate Limiting](#concurrency--rate-limiting)
- [Response Size Limiting](#response-size-limiting)
- [HTTP/2 and HTTP/3](#http2-and-http3)
- [Error Handling](#error-handling)
- [Request Deduplication](#request-deduplication)
- [Logging](#logging)
- [CDN / Browser Usage](#cdn--browser-usage)
- [Development](#development)
---
## Installation
```bash
npm install kinetex
```
### Optional peer dependencies
Install only what you need — kinetex works without any of them.
```bash
npm install undici # HTTP/2 + HTTP/3 in Node.js (auto-detected)
npm install zod # Schema validation
npm install valibot # Alternative schema validation
npm install socks-proxy-agent # SOCKS5 proxy support
```
---
## Quick Start
```ts
import kinetex from 'kinetex';
// Simple GET
const { data } = await kinetex.get('https://api.example.com/users/1');
console.log(data.name);
// POST with JSON body
const { data: created } = await kinetex.post('https://api.example.com/users', {
json: { name: 'Alice', email: 'alice@example.com' },
});
// Fluent chain
const { data } = await kinetex
.chain('https://api.example.com/users')
.bearer('my-token')
.query({ page: 1, limit: 20 })
.timeout(5000)
.retry(3);
```
---
## Runtime Support
kinetex works identically across all major JavaScript runtimes. Import it the same way everywhere.
### Node.js
```ts
import kinetex from 'kinetex'; // ESM ✅
const kinetex = require('kinetex'); // CJS ✅
const { data } = await kinetex.get('https://api.example.com/users');
```
### Deno
```ts
import kinetex, { create } from 'npm:kinetex';
// or via JSR:
import kinetex from 'jsr:@kinetexjs/kinetex';
const { data } = await kinetex.get('https://api.example.com/users');
```
> **Note:** `proxyMiddleware()` has no effect in Deno — configure proxies at the OS level or use the `HTTPS_PROXY` environment variable.
### Bun
```ts
import kinetex from 'kinetex'; // identical to Node.js
```
### Cloudflare Workers
```ts
import { create, auth } from 'kinetex';
const baseApi = create({ baseURL: 'https://api.example.com', timeout: 5000 });
export default {
async fetch(request: Request, env: Env): Promise {
const api = baseApi.extend(auth.bearer(env.API_TOKEN));
const { data } = await api.get('/users');
return Response.json(data);
},
};
interface Env { API_TOKEN: string; }
```
### Browser / CDN
```html
kinetex.get('https://api.example.com/data').then(({ data }) => console.log(data));
```
ESM in the browser:
```html
import kinetex from 'https://cdn.jsdelivr.net/npm/kinetex/dist/browser/kinetex.esm.min.js';
const { data } = await kinetex.get('https://api.example.com/posts');
```
---
## API Styles
### Async / Await
The default style — clean, typed, predictable.
```ts
import kinetex from 'kinetex';
// GET shorthand (call instance directly)
const { data } = await kinetex('https://api.example.com/users');
// HTTP verb methods
const { data } = await kinetex.get('/users/1');
const { data } = await kinetex.post('/users', { json: { name: 'Alice' } });
const { data } = await kinetex.put('/users/1', { json: { name: 'Bob' } });
const { data } = await kinetex.patch('/users/1', { json: { email: 'bob@example.com' } });
const { data } = await kinetex.delete('/users/1');
const { data } = await kinetex.head('/users');
const { data } = await kinetex.options('/users');
```
### Fluent Chain
Build requests incrementally. The chain is a `Promise` — `await` it when ready.
```ts
const { data } = await kinetex
.chain('https://api.example.com/users')
.method('POST')
.bearer('my-token')
.header('X-Request-ID', crypto.randomUUID())
.json({ name: 'Alice' })
.timeout(5000)
.retry(3);
```
**Chain method reference:**
| Method | Description |
|--------|-------------|
| `.method(m)` | HTTP method |
| `.header(k, v)` | Add a single header |
| `.headers(obj)` | Merge multiple headers |
| `.query(params)` | URL search params |
| `.send(body)` | Raw body |
| `.json(data)` | JSON body — sets `Content-Type: application/json` |
| `.form(data)` | URL-encoded form body |
| `.multipart(data)` | `multipart/form-data` (FormData or plain record) |
| `.auth(value)` | `Authorization` header verbatim |
| `.bearer(token)` | `Authorization: Bearer ` |
| `.basic(user, pass)` | `Authorization: Basic ` |
| `.timeout(ms)` | Request timeout |
| `.retry(n)` | Retry count |
| `.accept(type)` | `Accept` header |
| `.type(ct)` | `Content-Type` header |
| `.schema(v)` | Validate response with zod / valibot / custom |
| `.signal(s)` | `AbortSignal` for cancellation |
| `.cancel()` | Returns an `AbortController` wired to this request — call `.abort()` on it when you want to cancel |
| `.onUpload(fn)` | Upload progress callback |
| `.onDownload(fn)` | Download progress callback |
| `.as()` | Re-type the response generic |
```ts
// Multipart upload
const fd = new FormData();
fd.append('file', fileBlob, 'avatar.png');
await kinetex.chain('/upload').multipart(fd);
// Or from a plain record (auto-converted to FormData)
await kinetex.chain('/upload').multipart({ file: fileBlob, caption: 'photo' });
// Cancel a request — call controller.abort() when you actually want to cancel.
// cancel() does NOT abort immediately; the request fires lazily on first .then()
// so you need the controller to exist before it starts.
const chain = kinetex.chain('https://api.example.com/long-poll');
const controller = chain.cancel(); // wires the signal — NOT aborted yet
// ... start the request
const promise = chain; // triggers dispatch
// ... later:
controller.abort(); // now it cancels
```
### Callback
Drop-in compatible with the `request` package callback style.
```ts
kinetex.callback('https://api.example.com/data', {}, (err, res, data) => {
if (err) return console.error(err);
console.log(data);
});
```
---
## Instances & Configuration
Create isolated instances with their own defaults, middleware, and interceptors.
```ts
import { create } from 'kinetex';
const api = create({
baseURL: 'https://api.example.com',
timeout: 10_000,
retry: { limit: 3, delay: (n) => 100 * 2 ** n },
headers: { 'X-App': 'my-app/1.0' },
});
// Scoped sub-instance (inherits parent defaults)
const usersApi = api.create({ baseURL: 'https://api.example.com/users' });
// Extend with middleware — returns a new instance, does not mutate
const authedApi = api.extend(auth.bearer('secret'));
// Add middleware in-place — mutates this instance
api.use(myLoggingMiddleware);
// Cancel ALL in-flight requests on this instance (e.g. on component unmount)
api.cancelAll();
// The instance resets automatically — subsequent requests work normally
```
### Timeout granularity
```ts
// Simple: single number applies to the whole request lifecycle
await api.get('/data', { timeout: 5000 });
// Granular: separate timeouts for connection vs full response
await api.get('/data', {
timeout: {
request: 30_000, // abort if total request takes > 30s
response: 5_000, // abort if no first byte within 5s (TTFB guard)
},
});
```
---
## Request Config Reference
Every request method accepts a `RequestConfig` object. All fields are optional except `url`.
```ts
interface RequestConfig {
url: string;
method?: string; // default: 'GET'
// ── Headers & Body ─────────────────────────────────────────────────────
headers?: Record | [string,string][] | Headers;
body?: BodyInit; // raw body
json?: unknown; // serialised + Content-Type: application/json
form?: Record; // application/x-www-form-urlencoded
searchParams?: Record | URLSearchParams | string;
// ── Response ───────────────────────────────────────────────────────────
responseType?: 'json'|'text'|'blob'|'arrayBuffer'|'formData'|'stream';
schema?: ZodSchema | ValibotSchema | { parse(d: unknown): T }; // validates body
throwHttpErrors?: boolean; // default: true
decompress?: boolean; // default: true — set false to get raw compressed bytes
// ── Network ───────────────────────────────────────────────────────────
baseURL?: string;
timeout?: number | {
request?: number; // total request timeout (ms)
response?: number; // TTFB timeout — aborts if first byte not received in time
};
retry?: number | RetryConfig;
cache?: CacheConfig | false;
dedupe?: boolean; // default: true for GET
signal?: AbortSignal;
credentials?: RequestCredentials;
followRedirects?: boolean; // default: true
maxRedirects?: number; // default: 10
transport?: 'fetch' | 'undici'; // force transport
// ── Callbacks ─────────────────────────────────────────────────────────
onUploadProgress?: (event: ProgressEvent) => void; // Node + browser
onDownloadProgress?: (event: ProgressEvent) => void;
// ── Observability ─────────────────────────────────────────────────────
hooks?: HookConfig;
logger?: Logger;
har?: boolean;
}
```
---
## Response Object
Every request resolves to a `KinetexResponse`:
```ts
const res = await kinetex.get('/users/1');
res.data // T — parsed body
res.status // number — HTTP status code
res.statusText // string
res.headers // Headers — response headers
res.response // Response — original fetch Response (for streaming)
res.request // RequestConfig — originating request
res.fromCache // boolean — true if served from cache
res.retries // number — retry attempts made
res.timing // { start, end, duration, ttfb, ... }
res.harEntry // HarEntry — present when har: true
```
---
## Authentication
```ts
import { auth, create } from 'kinetex';
// Basic auth
create().extend(auth.basic('username', 'password'));
// Bearer token (static)
create().extend(auth.bearer('my-token'));
// Bearer token (async — refreshed on each request)
create().extend(auth.bearer(async () => getAccessToken()));
// API key in a header
create().extend(auth.apiKey('key-value', { header: 'X-API-Key' }));
// API key as a query param
create().extend(auth.apiKey('key-value', { query: 'api_key' }));
// OAuth 2.0 client credentials — auto-fetches and refreshes tokens
create().extend(auth.oauth2({
tokenUrl: 'https://auth.example.com/oauth/token',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
scope: 'read:users write:posts',
onToken: (token) => console.log('New token, expires in:', token.expires_in),
}));
// AWS SigV4 — signed using Web Crypto, no extra dependency
create().extend(auth.aws({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN, // optional
region: 'us-east-1',
service: 's3',
}));
// Digest auth — RFC 2617 compliant MD5 (nginx, Apache compatible)
create().extend(auth.digest('username', 'password'));
```
---
## Schema Validation
kinetex integrates with any schema library that exposes `.parse()` or `.safeParse()`.
### Zod
```ts
import { z } from 'zod';
// import * as z from 'zod';
import kinetex from 'kinetex';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// data is typed and validated at runtime — throws ValidationError on mismatch
const { data } = await kinetex.get('https://api.example.com/users/1', {
schema: UserSchema,
});
console.log(data.name); // string ✅
```
### Valibot
```ts
import * as v from 'valibot';
const UserSchema = v.object({ id: v.number(), name: v.string() });
const { data } = await kinetex.get('/users/1', { schema: UserSchema });
```
### Custom validator
Any object with `.parse()` or `.safeParse()` works:
```ts
const { data } = await kinetex.get('/users/1', {
schema: {
parse: (raw) => {
if (!raw || typeof raw !== 'object') throw new Error('Invalid');
return raw as User;
},
},
});
```
### Chain API
```ts
const { data } = await kinetex
.chain('/users/1')
.schema(UserSchema);
```
---
## Caching
Built-in in-memory cache with stale-while-revalidate support. Works with all response types.
The default cache store is **per-instance** — each `create()` call gets its own isolated store, so cache entries from one instance never bleed into another. To share a cache across instances, pass a custom `store` explicitly.
```ts
// Cache for 60s, serve stale for up to 5 more minutes while revalidating in the background
const { data, fromCache } = await kinetex.get('/api/config', {
cache: { ttl: 60_000, swr: 300_000 },
});
console.log('from cache?', fromCache);
// Opt out for one request
await api.get('/realtime-data', { cache: false });
// Custom cache key
await api.get('/users', {
cache: {
ttl: 30_000,
key: (req) => `v2:${req.url}`,
},
});
```
### Custom store (e.g. Redis)
```ts
import { create } from 'kinetex';
const api = create({
cache: {
ttl: 30_000,
store: {
get: (key) => redis.get(key).then(v => v ? JSON.parse(v) : undefined),
set: (key, entry) => redis.setex(
key,
Math.ceil((entry.expiresAt - Date.now()) / 1000),
JSON.stringify(entry)
),
delete: (key) => redis.del(key),
clear: () => redis.flushdb(),
},
},
});
```
---
## Retry
```ts
const { data } = await kinetex.get('/api/data', {
retry: {
limit: 4,
statusCodes: [429, 500, 502, 503, 504],
methods: ['GET', 'POST'],
delay: (attempt) => Math.min(200 * 2 ** attempt, 10_000), // exponential back-off
onNetworkError: true,
onRetry: (attempt, err, req) => console.warn(`Retry ${attempt} for ${req.url}:`, err),
},
});
// Shorthand — retries 3 times with defaults
const { data } = await kinetex.get('/api/data', { retry: 3 });
```
---
## Interceptors
Axios-compatible interceptor API. Interceptors run as part of the request pipeline.
```ts
import { create } from 'kinetex';
const api = create({ baseURL: 'https://api.example.com' });
// Request interceptor — add a tracing header to every request
const reqId = api.interceptors.request.use(async (config) => ({
...config,
headers: { ...config.headers, 'X-Request-ID': crypto.randomUUID() },
}));
// Response interceptor — unwrap a nested payload
const resId = api.interceptors.response.use((response) => ({
...response,
data: response.data?.payload ?? response.data,
}));
// Remove individual interceptors
api.interceptors.request.eject(reqId);
api.interceptors.response.eject(resId);
```
---
## Lifecycle Hooks
Fine-grained hooks for observability, modification, and error handling. Set per-request or on an instance.
```ts
const { data } = await kinetex.get('/api/data', {
hooks: {
// Modify the request before it is sent
beforeRequest: [
(config) => ({ ...config, url: config.url + '?source=kinetex' }),
],
// Inspect or transform the response
afterResponse: [
(response) => {
console.log(`${response.status} in ${response.timing.duration}ms`);
return response;
},
],
// React to any error (does not suppress it)
onError: [
(err) => metrics.increment('http.error', { status: err.response?.status }),
],
},
});
```
---
## Middleware Pipeline
The full power of kinetex — compose arbitrary request/response transforms into the pipeline.
```ts
import { create, compose } from 'kinetex';
import type { Middleware } from 'kinetex';
// Write a middleware
const timingMiddleware: Middleware = async (request, next) => {
const start = Date.now();
const response = await next(request);
console.log(`${request.method} ${request.url} — ${Date.now() - start}ms`);
return response;
};
const api = create({ baseURL: 'https://api.example.com' });
api.use(timingMiddleware);
// Compose multiple middlewares manually
const pipeline = compose([timingMiddleware, authMiddleware], coreHandler);
```
---
## SSE Streaming
Server-Sent Events with automatic reconnection, `Last-Event-ID` tracking, and server-side `retry:` interval support.
```ts
import { sse } from 'kinetex/plugins';
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000); // 30s limit
for await (const event of sse('https://api.example.com/events', {
signal: controller.signal,
headers: { Authorization: 'Bearer my-token' },
maxRetries: 10, // Infinity by default
})) {
console.log(`[${event.event ?? 'message'}]`, event.data);
if (event.id) console.log('event id:', event.id);
if (event.event === 'done') break;
}
```
**SSE event fields:**
```ts
interface SSEEvent {
data: string; // event data
event?: string; // event type (from "event:" line)
id?: string; // event id (from "id:" line)
retry?: number; // retry interval in ms (from "retry:" line)
}
```
---
## GraphQL
Type-safe GraphQL requests with automatic error detection.
```ts
import { create } from 'kinetex';
import { graphqlPlugin } from 'kinetex/plugins';
const api = create({ baseURL: 'https://api.example.com' });
interface UserData { user: { id: string; name: string } }
interface UserVars { id: string }
const config = graphqlPlugin('/graphql', {
query: `
query GetUser($id: ID!) {
user(id: $id) { id name }
}
`,
variables: { id: '42' },
operationName: 'GetUser',
});
const { data } = await api.post(config.url, config);
// Throws automatically if data.errors is present
console.log(data.data?.user.name);
```
---
## File Upload & Progress
### Multipart upload
```ts
const form = new FormData();
form.append('title', 'My Upload');
form.append('file', fileBlob, 'document.pdf');
const { data } = await kinetex.post('/upload', { body: form });
```
### Upload progress
In Node.js, `onUploadProgress` wraps the request body in a progress-tracking `ReadableStream` that emits events as undici reads each chunk. In Bun, upload progress uses Bun's native fetch streaming. In the browser, kinetex automatically falls back to `XMLHttpRequest` to provide real upload progress events.
```ts
import type { ProgressEvent as KinetexProgressEvent } from 'kinetex';
await kinetex.post('/upload', {
body: fileBuffer,
onUploadProgress: (event: KinetexProgressEvent) => {
console.log(`${event.percent?.toFixed(0)}% — ${(event.bytesPerSecond / 1024).toFixed(1)} KB/s`);
},
});
```
### Download progress
```ts
await kinetex.get('/large-file.zip', {
responseType: 'blob',
onDownloadProgress: ({ loaded, total, percent }) => {
progressBar.value = percent ?? 0;
},
});
```
**`ProgressEvent` fields:**
```ts
interface ProgressEvent {
loaded: number; // bytes transferred so far
total: number | undefined; // total bytes (undefined if unknown)
percent: number | undefined; // 0–100 (undefined if total unknown)
transferredBytes: number; // same as loaded
bytesPerSecond: number; // current transfer speed
}
```
---
## HAR Recording
Record all HTTP traffic as a [HAR 1.2](https://www.softwareishard.com/blog/har-12-spec/) file for debugging, testing, or replaying.
```ts
import { create } from 'kinetex';
import { writeFileSync } from 'node:fs';
const api = create();
await api.get('https://api.example.com/users', { har: true });
await api.post('https://api.example.com/posts', { json: { title: 'test' }, har: true });
const har = api.exportHAR();
// har.log.version === '1.2'
// har.log.entries — array of HarEntry
writeFileSync('trace.har', JSON.stringify(har, null, 2));
// Open in Chrome DevTools → Network → Import HAR
```
---
## Cookie Jar
RFC 6265 compliant cookie jar — no dependencies.
```ts
import { create } from 'kinetex';
import { cookieJar, withCookies } from 'kinetex/plugins';
const jar = cookieJar();
const api = create().use(withCookies(jar));
// Cookies from Set-Cookie headers are stored automatically
await api.get('https://example.com/login');
// Stored cookies are sent automatically on subsequent requests
await api.get('https://example.com/dashboard');
// Manual jar operations
const all = jar.getAll(); // all stored cookies
const header = jar.getCookieString('https://example.com/'); // "name=value; ..."
jar.delete('session', 'example.com', '/'); // remove one cookie
jar.clear(); // remove all
```
---
## Proxy & SOCKS5
> **Node.js and Bun only.** Calling proxy middleware in browser, edge, or Deno environments logs a `console.warn` and falls through — there is no silent failure.
```ts
import { create } from 'kinetex';
import { proxyMiddleware, envProxy } from 'kinetex/plugins';
// Explicit HTTP/HTTPS proxy
const api = create().use(proxyMiddleware({
url: 'http://proxy.corp.internal:3128',
auth: { username: 'user', password: 'pass' },
noProxy: ['internal.corp.com', '.local'],
}));
// Read from environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
const api2 = create().use(envProxy());
// SOCKS5 proxy — requires: npm install socks-proxy-agent
const api3 = create().use(proxyMiddleware({
url: 'socks5://proxy.example.com:1080',
protocol: 'socks5',
}));
```
**`ProxyConfig` fields:**
```ts
interface ProxyConfig {
url?: string; // proxy URL
protocol?: 'http' | 'https' | 'socks5';
auth?: { username: string; password: string };
noProxy?: string[]; // hostnames / patterns to bypass
headers?: Record; // extra headers sent to proxy
}
```
---
## Concurrency & Rate Limiting
```ts
import { create } from 'kinetex';
import { concurrencyLimit, rateLimit } from 'kinetex/plugins';
const api = create({ baseURL: 'https://api.example.com' })
.use(concurrencyLimit(5)) // max 5 in-flight requests
.use(rateLimit({ requestsPerSecond: 10, burst: 20 })); // token-bucket rate limiter
```
**`rateLimit` options:**
| Option | Type | Description |
|--------|------|-------------|
| `requestsPerSecond` | `number` | Sustained rate |
| `burst` | `number` | Max burst size (default: `requestsPerSecond`) |
---
## Response Size Limiting
Protect against unexpectedly large responses. Checks `Content-Length` header first, then falls back to measuring the actual body.
```ts
import { create, ResponseSizeError } from 'kinetex/plugins';
import { responseSizeLimit } from 'kinetex/plugins';
const api = create().use(responseSizeLimit(5 * 1024 * 1024)); // 5 MB limit
try {
const { data } = await api.get('/large-file');
} catch (err) {
if (err instanceof ResponseSizeError) {
console.log(`Too large: ${err.actualBytes} bytes (limit: ${err.maxBytes})`);
}
}
```
---
## HTTP/2 and HTTP/3
```bash
npm install undici
```
When `undici` is installed, kinetex automatically uses it in Node.js for HTTP/2 and HTTP/3 support. Bun uses its own optimized native fetch. Falls back to native `fetch` transparently when unavailable.
Connections to each origin are pooled via a persistent `undici.Pool` (10 connections per origin, keep-alive 30s). This means multiple requests to the same host reuse the same TCP/TLS/HTTP2 session — true multiplexing, not just HTTP/2 framing.
```ts
// Force native fetch for one request (e.g. when streaming to a non-undici consumer)
const { data } = await kinetex.get('/api', { transport: 'fetch' });
// Force undici for all requests on this instance
const api = create({ transport: 'undici' });
// Opt out of automatic response decompression (get raw compressed bytes)
const { data } = await kinetex.get('/compressed', { decompress: false });
```
> **Note:** connection pooling is per-process, not per-instance. All kinetex instances share the same `undici.Pool` for a given origin.
---
## Error Handling
All errors extend `KinetexError`. Catch the specific subclass you care about.
```ts
import { HTTPError, TimeoutError, ValidationError, KinetexError } from 'kinetex';
try {
await kinetex.get('https://api.example.com/protected');
} catch (err) {
if (err instanceof HTTPError) {
console.log(err.response?.status); // 401, 404, 429 ...
console.log(err.response?.data); // parsed error body
console.log(err.request.url); // originating URL
} else if (err instanceof TimeoutError) {
console.log('Timed out:', err.message);
} else if (err instanceof ValidationError) {
console.log('Schema mismatch:', err.validationError);
} else if (err instanceof KinetexError) {
console.log('kinetex error:', err.originalCause);
}
}
```
### Opt out of throwing on non-2xx responses
```ts
const res = await kinetex.get('/api/data', { throwHttpErrors: false });
if (res.status === 404) {
console.log('not found');
}
```
**Error types:**
| Class | When thrown |
|-------|-------------|
| `KinetexError` | Base class for all errors |
| `HTTPError` | Non-2xx response (when `throwHttpErrors: true`) |
| `TimeoutError` | Request exceeded `timeout` |
| `ValidationError` | Response failed schema validation |
| `ResponseSizeError` | Response exceeded `responseSizeLimit` |
---
## Request Deduplication
Identical in-flight GET requests on the **same instance** are collapsed into a single network call. All callers receive the same response.
```ts
const api = create({ baseURL: 'https://api.example.com' });
// All three fire simultaneously — only one HTTP request is made
const [r1, r2, r3] = await Promise.all([
api.get('/config'),
api.get('/config'),
api.get('/config'),
]);
// Opt out per-request
await api.get('/always-fresh', { dedupe: false });
```
Deduplication state is **instance-scoped** — two separate `create()` calls never share inflight maps.
**Authorization-aware:** the dedup key includes the `Authorization` header value, so two requests to the same URL but with different credentials (e.g. different user tokens in a multi-tenant server) are never collapsed into the same in-flight request.
---
## Logging
Attach a logger to an instance or individual request for structured observability.
```ts
import { create } from 'kinetex';
import type { Logger } from 'kinetex';
const logger: Logger = {
request: (config) => console.log(`→ ${config.method} ${config.url}`),
response: (res) => console.log(`← ${res.status} ${res.timing.duration}ms`),
error: (err) => console.error(`✗ ${err.message}`),
};
// On an instance
const api = create({ logger });
// Or per-request
await kinetex.get('/api/data', { logger });
```
---
## Development
```bash
git clone https://github.com/kinetexjs/kinetex.git
cd kinetex && npm install
npm run typecheck # TypeScript — 0 errors
npm run lint # ESLint — 0 warnings
npm run build # ESM + CJS + types + browser bundles
npm test # ~149 tests, 0 failures (core suite)
npm run test:all # ~460 tests, 0 failures (full suite)
npm run test:coverage # coverage report (≥ 95% ESM line coverage)
npm run bench # benchmark vs native fetch and node:http (add more: npm install --no-save axios got ky undici)
# Run individual runtimes
npm run test:bun
npm run test:deno
# Generate local API docs
npm run docs
open docs/index.html
# Create a release
node scripts/release.mjs patch # or minor / major
git push && git push --tags
```
---
## License
[MIT](LICENSE) © Qasim Ali
