httix-http 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1184 -0
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +592 -0
- package/dist/cjs/plugins/index.cjs +2 -0
- package/dist/cjs/plugins/index.cjs.map +1 -0
- package/dist/cjs/plugins/index.d.cts +154 -0
- package/dist/cjs/types-DkChbb42.d.cts +340 -0
- package/dist/esm/index.d.ts +592 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugins/index.d.ts +154 -0
- package/dist/esm/plugins/index.js +2 -0
- package/dist/esm/plugins/index.js.map +1 -0
- package/dist/esm/types-DkChbb42.d.ts +340 -0
- package/package.json +114 -0
package/README.md
ADDED
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/npm/v/httix-http?style=flat-square&color=blue" alt="npm version" />
|
|
3
|
+
<img src="https://img.shields.io/npm/l/httix?style=flat-square&color=green" alt="MIT License" />
|
|
4
|
+
<img src="https://img.shields.io/badge/TypeScript-5.7+-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
5
|
+
<img src="https://img.shields.io/badge/bundle_size-~5kB_min%2Bgzip-orange?style=flat-square" alt="Bundle Size" />
|
|
6
|
+
<img src="https://img.shields.io/badge/zero_dependencies-brightgreen?style=flat-square" alt="Zero Dependencies" />
|
|
7
|
+
<img src="https://img.shields.io/badge/coverage-100%25-success?style=flat-square" alt="100% Coverage" />
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<h1 align="center">httix</h1>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<strong>Ultra-lightweight, type-safe, zero-dependency HTTP client built on native Fetch.</strong><br/>
|
|
14
|
+
The modern axios replacement for the JavaScript ecosystem.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why httix?
|
|
20
|
+
|
|
21
|
+
| Feature | **httix** | axios | got | ky | ofetch |
|
|
22
|
+
|---|---|---|---|---|---|
|
|
23
|
+
| Dependencies | **0** | 2 | 11 | 2 | 5 |
|
|
24
|
+
| Size (min+gzip) | **~5 kB** | ~28 kB | ~67 kB | ~9 kB | ~12 kB |
|
|
25
|
+
| Built on Fetch API | ✅ | ❌ | ❌ | ✅ | ✅ |
|
|
26
|
+
| TypeScript native | ✅ | ⚠️ (v1 types) | ✅ | ✅ | ✅ |
|
|
27
|
+
| Interceptors | ✅ | ✅ | ✅ | ❌ | ✅ |
|
|
28
|
+
| Retry with backoff | ✅ | ❌ (plugin) | ✅ | ✅ | ✅ |
|
|
29
|
+
| Request deduplication | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
30
|
+
| Rate limiting | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
31
|
+
| Middleware pipeline | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
32
|
+
| Auth helpers | ✅ | ❌ (plugin) | ✅ | ❌ | ❌ |
|
|
33
|
+
| Auto-pagination | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
34
|
+
| SSE / NDJSON streaming | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
35
|
+
| Cache plugin | ✅ | ❌ (adapter) | ✅ | ✅ | ✅ |
|
|
36
|
+
| Mock plugin (testing) | ✅ | ✅ (adapter) | ✅ | ❌ | ❌ |
|
|
37
|
+
| Response timing | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
38
|
+
| Cancel all requests | ✅ | ⚠️ (manual) | ✅ | ✅ | ❌ |
|
|
39
|
+
| ESM + CJS | ✅ | ✅ | ❌ (ESM) | ✅ | ✅ |
|
|
40
|
+
| Runtime agnostic | ✅ | ✅ | Node only | Browser | Universal |
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# npm
|
|
46
|
+
npm install httix-http
|
|
47
|
+
|
|
48
|
+
# yarn
|
|
49
|
+
yarn add httix-http
|
|
50
|
+
|
|
51
|
+
# pnpm
|
|
52
|
+
pnpm add httix-http
|
|
53
|
+
|
|
54
|
+
# bun
|
|
55
|
+
bun add httix-http
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### 1. Simple GET request
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import httix from 'httix-http';
|
|
64
|
+
|
|
65
|
+
const { data, status, timing } = await httix.get('/users');
|
|
66
|
+
console.log(data); // parsed JSON response
|
|
67
|
+
console.log(status); // 200
|
|
68
|
+
console.log(timing); // request duration in ms
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. POST with JSON body
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import httix from 'httix-http';
|
|
75
|
+
|
|
76
|
+
const { data } = await httix.post('/users', {
|
|
77
|
+
name: 'Avinash',
|
|
78
|
+
email: 'avinash@example.com',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log(data.id); // created user id
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Create a configured client
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createHttix } from 'httix';
|
|
88
|
+
|
|
89
|
+
const api = createHttix({
|
|
90
|
+
baseURL: 'https://api.example.com',
|
|
91
|
+
auth: { type: 'bearer', token: 'my-secret-token' },
|
|
92
|
+
headers: { 'X-App-Version': '1.0.0' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const { data } = await api.get('/users/me');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Error handling
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import httix, { HttixResponseError, HttixTimeoutError, HttixAbortError } from 'httix';
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const { data } = await httix.get('/users/999');
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof HttixResponseError) {
|
|
107
|
+
console.error(`Server error: ${error.status} — ${error.statusText}`);
|
|
108
|
+
console.error('Response body:', error.data);
|
|
109
|
+
} else if (error instanceof HttixTimeoutError) {
|
|
110
|
+
console.error(`Request timed out after ${error.timeout}ms`);
|
|
111
|
+
} else if (error instanceof HttixAbortError) {
|
|
112
|
+
console.error('Request was cancelled');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 5. Streaming SSE events
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import httix from 'httix';
|
|
121
|
+
|
|
122
|
+
for await (const event of httix.stream.sse('/events')) {
|
|
123
|
+
console.log(`[${event.type}] ${event.data}`);
|
|
124
|
+
if (event.type === 'done') break;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## API Reference
|
|
131
|
+
|
|
132
|
+
### Creating Instances
|
|
133
|
+
|
|
134
|
+
#### `createHttix(config?)`
|
|
135
|
+
|
|
136
|
+
Create a new client instance with the given configuration. This is the recommended entry-point for creating dedicated API clients.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { createHttix } from 'httix';
|
|
140
|
+
|
|
141
|
+
const api = createHttix({
|
|
142
|
+
baseURL: 'https://api.example.com/v2',
|
|
143
|
+
headers: {
|
|
144
|
+
'X-App-Version': '1.0.0',
|
|
145
|
+
'Accept-Language': 'en-US',
|
|
146
|
+
},
|
|
147
|
+
timeout: 15000,
|
|
148
|
+
retry: { attempts: 5, backoff: 'exponential' },
|
|
149
|
+
auth: { type: 'bearer', token: 'my-token' },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const { data } = await api.get('/users');
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `httix.create(config?)`
|
|
156
|
+
|
|
157
|
+
Create a derived client from the default instance, merging new configuration with the defaults:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import httix from 'httix';
|
|
161
|
+
|
|
162
|
+
const adminApi = httix.create({
|
|
163
|
+
baseURL: 'https://admin.api.example.com',
|
|
164
|
+
auth: { type: 'bearer', token: adminToken },
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Default instance
|
|
169
|
+
|
|
170
|
+
A pre-configured default instance is exported for convenience:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import httix from 'httix';
|
|
174
|
+
|
|
175
|
+
// Use directly
|
|
176
|
+
await httix.get('/users');
|
|
177
|
+
|
|
178
|
+
// Destructure
|
|
179
|
+
const { get, post, put, patch, delete: remove } = httix;
|
|
180
|
+
await get('/users');
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### HTTP Methods
|
|
184
|
+
|
|
185
|
+
All methods return `Promise<HttixResponse<T>>` and support a generic type parameter for the response body.
|
|
186
|
+
|
|
187
|
+
#### `httix.get<T>(url, config?)`
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const users = await httix.get<User[]>('/users');
|
|
191
|
+
console.log(users.data); // User[]
|
|
192
|
+
|
|
193
|
+
// With query parameters
|
|
194
|
+
const page = await httix.get<User[]>('/users', {
|
|
195
|
+
query: { page: 1, limit: 20, active: true },
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `httix.post<T>(url, body?, config?)`
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const user = await httix.post<User>('/users', {
|
|
203
|
+
name: 'Jane',
|
|
204
|
+
email: 'jane@example.com',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// With FormData
|
|
208
|
+
const form = new FormData();
|
|
209
|
+
form.append('avatar', fileInput.files[0]);
|
|
210
|
+
const upload = await httix.post('/upload', form);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### `httix.put<T>(url, body?, config?)`
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const updated = await httix.put<User>('/users/1', {
|
|
217
|
+
name: 'Jane Updated',
|
|
218
|
+
email: 'jane@newdomain.com',
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### `httix.patch<T>(url, body?, config?)`
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const patched = await httix.patch<User>('/users/1', { name: 'Jane v2' });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `httix.delete<T>(url, config?)`
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const result = await httix.delete<{ deleted: boolean }>('/users/1');
|
|
232
|
+
console.log(result.data.deleted); // true
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### `httix.head(url, config?)`
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const headers = await httix.head('/large-file.pdf');
|
|
239
|
+
console.log(headers.headers.get('content-length')); // "1048576"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### `httix.options(url, config?)`
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const allowed = await httix.options('/api');
|
|
246
|
+
console.log(allowed.headers.get('allow')); // "GET, POST, OPTIONS"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### `httix.request<T>(config)`
|
|
250
|
+
|
|
251
|
+
The underlying method for all HTTP shortcuts. Use it for maximum control:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const { data } = await httix.request<User>({
|
|
255
|
+
method: 'POST',
|
|
256
|
+
url: '/users',
|
|
257
|
+
body: { name: 'Jane' },
|
|
258
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
259
|
+
timeout: 5000,
|
|
260
|
+
retry: { attempts: 2 },
|
|
261
|
+
query: { verify: true },
|
|
262
|
+
responseType: 'json',
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### The Response Object
|
|
267
|
+
|
|
268
|
+
Every method returns an `HttixResponse<T>`:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
interface HttixResponse<T> {
|
|
272
|
+
data: T; // Parsed response body
|
|
273
|
+
status: number; // HTTP status code (e.g. 200)
|
|
274
|
+
statusText: string; // HTTP status text (e.g. "OK")
|
|
275
|
+
headers: Headers; // Native Headers object
|
|
276
|
+
ok: boolean; // true if status is 2xx
|
|
277
|
+
raw: Response; // Original Fetch Response
|
|
278
|
+
timing: number; // Request duration in ms
|
|
279
|
+
config: HttixRequestConfig; // Config that produced this response
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const response = await httix.get('/users');
|
|
285
|
+
console.log(response.data); // parsed body
|
|
286
|
+
console.log(response.status); // 200
|
|
287
|
+
console.log(response.ok); // true
|
|
288
|
+
console.log(response.timing); // 142 (ms)
|
|
289
|
+
console.log(response.headers.get('x-ratelimit-remaining')); // "99"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Interceptors
|
|
293
|
+
|
|
294
|
+
Interceptors let you run logic before a request is sent or after a response is received. They are identical in concept to axios interceptors.
|
|
295
|
+
|
|
296
|
+
#### Request Interceptor
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// Add a request ID and timestamp to every outgoing request
|
|
300
|
+
httix.interceptors.request.use((config) => {
|
|
301
|
+
config.headers = config.headers ?? {};
|
|
302
|
+
if (config.headers instanceof Headers) {
|
|
303
|
+
config.headers.set('X-Request-ID', crypto.randomUUID());
|
|
304
|
+
} else {
|
|
305
|
+
config.headers['X-Request-ID'] = crypto.randomUUID();
|
|
306
|
+
}
|
|
307
|
+
return config;
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Response Interceptor
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
// Transform response data
|
|
315
|
+
httix.interceptors.response.use((response) => {
|
|
316
|
+
// Wrap data in an envelope
|
|
317
|
+
response.data = { success: true, data: response.data };
|
|
318
|
+
return response;
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Response Error Interceptor
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
// Handle 401 globally — attempt token refresh
|
|
326
|
+
httix.interceptors.response.use(
|
|
327
|
+
(response) => response,
|
|
328
|
+
(error) => {
|
|
329
|
+
if (error instanceof HttixResponseError && error.status === 401) {
|
|
330
|
+
console.error('Unauthorized — redirecting to login');
|
|
331
|
+
window.location.href = '/login';
|
|
332
|
+
}
|
|
333
|
+
// Return void to let the error propagate
|
|
334
|
+
return;
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Ejecting Interceptors
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
const id = httix.interceptors.request.use((config) => {
|
|
343
|
+
config.headers = config.headers ?? {};
|
|
344
|
+
if (config.headers instanceof Headers) {
|
|
345
|
+
config.headers.set('X-Trace', 'enabled');
|
|
346
|
+
} else {
|
|
347
|
+
config.headers['X-Trace'] = 'enabled';
|
|
348
|
+
}
|
|
349
|
+
return config;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Remove the interceptor later
|
|
353
|
+
httix.interceptors.request.eject(id);
|
|
354
|
+
|
|
355
|
+
// Clear all interceptors
|
|
356
|
+
httix.interceptors.request.clear();
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Retry Configuration
|
|
360
|
+
|
|
361
|
+
Automatic retry with configurable backoff strategies is built-in and enabled by default.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { createHttix } from 'httix';
|
|
365
|
+
|
|
366
|
+
const client = createHttix({
|
|
367
|
+
baseURL: 'https://api.example.com',
|
|
368
|
+
retry: {
|
|
369
|
+
attempts: 5, // Max retry attempts (default: 3)
|
|
370
|
+
backoff: 'exponential', // 'fixed' | 'linear' | 'exponential'
|
|
371
|
+
baseDelay: 1000, // Base delay in ms (default: 1000)
|
|
372
|
+
maxDelay: 30000, // Max delay cap in ms (default: 30000)
|
|
373
|
+
jitter: true, // Add randomness to prevent thundering herd (default: true)
|
|
374
|
+
retryOn: [408, 429, 500, 502, 503, 504], // Status codes to retry (default)
|
|
375
|
+
retryOnNetworkError: true, // Retry on DNS/network failures (default: true)
|
|
376
|
+
retryOnSafeMethodsOnly: false, // Only retry GET/HEAD/OPTIONS (default: false)
|
|
377
|
+
retryCondition: (error) => { // Custom retry condition
|
|
378
|
+
// Don't retry if the response contains a specific error code
|
|
379
|
+
if (error instanceof HttixResponseError && error.data?.code === 'NO_RETRY') {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
},
|
|
384
|
+
onRetry: (attempt, error, delay) => { // Callback before each retry
|
|
385
|
+
console.warn(`Retry attempt ${attempt} after ${delay}ms — ${error.message}`);
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Disable retry for a single request:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
const { data } = await httix.get('/ephemeral', { retry: false });
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Timeout & Abort
|
|
398
|
+
|
|
399
|
+
#### Timeout
|
|
400
|
+
|
|
401
|
+
Every request has a default 30-second timeout. Override per-request or globally:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// Per-request timeout
|
|
405
|
+
const { data } = await httix.get('/slow-endpoint', { timeout: 5000 });
|
|
406
|
+
|
|
407
|
+
// Global timeout
|
|
408
|
+
const client = createHttix({ timeout: 10000 });
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### Abort with AbortController
|
|
412
|
+
|
|
413
|
+
Cancel individual requests using a standard `AbortController`:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const controller = new AbortController();
|
|
417
|
+
|
|
418
|
+
// Cancel after 2 seconds
|
|
419
|
+
setTimeout(() => controller.abort(), 2000);
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const { data } = await httix.get('/large-dataset', {
|
|
423
|
+
signal: controller.signal,
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (httix.isCancel(error)) {
|
|
427
|
+
console.log('Request was cancelled by the user');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### Cancel all in-flight requests
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
// Cancel every pending request on this client
|
|
436
|
+
httix.cancelAll('User navigated away');
|
|
437
|
+
|
|
438
|
+
// Check if an error is from cancellation
|
|
439
|
+
try {
|
|
440
|
+
await httix.get('/data');
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (httix.isCancel(error)) {
|
|
443
|
+
console.log(error.reason); // "User navigated away"
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Streaming
|
|
449
|
+
|
|
450
|
+
#### Server-Sent Events (SSE)
|
|
451
|
+
|
|
452
|
+
Stream SSE events as an async iterable:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import httix from 'httix';
|
|
456
|
+
|
|
457
|
+
for await (const event of httix.stream.sse('https://api.example.com/events', {
|
|
458
|
+
headers: { 'Accept': 'text/event-stream' },
|
|
459
|
+
})) {
|
|
460
|
+
console.log(`[Event: ${event.type}]`, event.data);
|
|
461
|
+
|
|
462
|
+
if (event.id) {
|
|
463
|
+
console.log(`Last event ID: ${event.id}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (event.type === 'shutdown') break;
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### NDJSON Streaming
|
|
471
|
+
|
|
472
|
+
Stream newline-delimited JSON objects:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import httix from 'httix';
|
|
476
|
+
|
|
477
|
+
interface LogEntry {
|
|
478
|
+
timestamp: string;
|
|
479
|
+
level: string;
|
|
480
|
+
message: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for await (const entry of httix.stream.ndjson<LogEntry>('/logs/stream')) {
|
|
484
|
+
console.log(`[${entry.level}] ${entry.message}`);
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Request Deduplication
|
|
489
|
+
|
|
490
|
+
Automatically deduplicate identical in-flight requests. When enabled, if multiple calls are made with the same config before the first resolves, they share the same promise.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import { createHttix } from 'httix';
|
|
494
|
+
|
|
495
|
+
const client = createHttix({
|
|
496
|
+
baseURL: 'https://api.example.com',
|
|
497
|
+
dedup: true,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Both calls will share the same underlying request
|
|
501
|
+
const [users1, users2] = await Promise.all([
|
|
502
|
+
client.get('/users'),
|
|
503
|
+
client.get('/users'),
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
// Advanced configuration
|
|
507
|
+
const client2 = createHttix({
|
|
508
|
+
dedup: {
|
|
509
|
+
enabled: true,
|
|
510
|
+
ttl: 60000, // Cache dedup result for 60s
|
|
511
|
+
generateKey: (config) => `${config.method}:${config.url}`,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Rate Limiting
|
|
517
|
+
|
|
518
|
+
Client-side rate limiting to avoid overwhelming APIs:
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import { createHttix } from 'httix';
|
|
522
|
+
|
|
523
|
+
const client = createHttix({
|
|
524
|
+
baseURL: 'https://rate-limited-api.example.com',
|
|
525
|
+
rateLimit: {
|
|
526
|
+
maxRequests: 10, // Max 10 requests
|
|
527
|
+
interval: 1000, // Per 1 second window
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Requests will be automatically throttled
|
|
532
|
+
const results = await Promise.all([
|
|
533
|
+
client.get('/resource/1'),
|
|
534
|
+
client.get('/resource/2'),
|
|
535
|
+
client.get('/resource/3'),
|
|
536
|
+
// ... up to 10 concurrent, rest queued
|
|
537
|
+
]);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Middleware
|
|
541
|
+
|
|
542
|
+
Middleware functions have access to both the request and response, and can modify either:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
import httix, { type MiddlewareFn, type MiddlewareContext } from 'httix';
|
|
546
|
+
|
|
547
|
+
// Timing middleware
|
|
548
|
+
const timingMiddleware: MiddlewareFn = async (ctx, next) => {
|
|
549
|
+
const start = Date.now();
|
|
550
|
+
await next();
|
|
551
|
+
const duration = Date.now() - start;
|
|
552
|
+
console.log(`[Timing] ${ctx.request.method} ${ctx.request.url} — ${duration}ms`);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Request/response logging middleware
|
|
556
|
+
const loggingMiddleware: MiddlewareFn = async (ctx, next) => {
|
|
557
|
+
console.log(`>> ${ctx.request.method} ${ctx.request.url}`);
|
|
558
|
+
await next();
|
|
559
|
+
if (ctx.response) {
|
|
560
|
+
console.log(`<< ${ctx.response.status} ${ctx.response.statusText}`);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// Register middleware
|
|
565
|
+
httix.use(timingMiddleware);
|
|
566
|
+
httix.use(loggingMiddleware);
|
|
567
|
+
|
|
568
|
+
// Middleware is also configurable at client creation
|
|
569
|
+
const client = httix.create({
|
|
570
|
+
middleware: [timingMiddleware, loggingMiddleware],
|
|
571
|
+
});
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Auth
|
|
575
|
+
|
|
576
|
+
#### Bearer Auth
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import { createHttix } from 'httix';
|
|
580
|
+
|
|
581
|
+
// Static token
|
|
582
|
+
const client = createHttix({
|
|
583
|
+
baseURL: 'https://api.example.com',
|
|
584
|
+
auth: { type: 'bearer', token: 'my-jwt-token' },
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Dynamic token (e.g., from a store)
|
|
588
|
+
const client2 = createHttix({
|
|
589
|
+
baseURL: 'https://api.example.com',
|
|
590
|
+
auth: {
|
|
591
|
+
type: 'bearer',
|
|
592
|
+
token: () => localStorage.getItem('access_token') ?? '',
|
|
593
|
+
refreshToken: async () => {
|
|
594
|
+
const res = await fetch('/auth/refresh', { method: 'POST' });
|
|
595
|
+
const { accessToken } = await res.json();
|
|
596
|
+
localStorage.setItem('access_token', accessToken);
|
|
597
|
+
return accessToken;
|
|
598
|
+
},
|
|
599
|
+
onTokenRefresh: (token) => {
|
|
600
|
+
localStorage.setItem('access_token', token);
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
#### Basic Auth
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
const client = createHttix({
|
|
610
|
+
baseURL: 'https://api.example.com',
|
|
611
|
+
auth: {
|
|
612
|
+
type: 'basic',
|
|
613
|
+
username: 'admin',
|
|
614
|
+
password: 'secret',
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
#### API Key Auth
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
// API key in header
|
|
623
|
+
const client = createHttix({
|
|
624
|
+
baseURL: 'https://api.example.com',
|
|
625
|
+
auth: {
|
|
626
|
+
type: 'apiKey',
|
|
627
|
+
key: 'X-API-Key',
|
|
628
|
+
value: 'my-api-key',
|
|
629
|
+
in: 'header',
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// API key in query string
|
|
634
|
+
const client2 = createHttix({
|
|
635
|
+
baseURL: 'https://api.example.com',
|
|
636
|
+
auth: {
|
|
637
|
+
type: 'apiKey',
|
|
638
|
+
key: 'api_key',
|
|
639
|
+
value: 'my-api-key',
|
|
640
|
+
in: 'query',
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Pagination
|
|
646
|
+
|
|
647
|
+
Automatically fetch all pages of a paginated resource:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
import { createHttix } from 'httix';
|
|
651
|
+
|
|
652
|
+
const client = createHttix({ baseURL: 'https://api.example.com' });
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### Offset-based pagination
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
for await (const page of client.paginate<User>('/users', {
|
|
659
|
+
pagination: {
|
|
660
|
+
style: 'offset',
|
|
661
|
+
pageSize: 50,
|
|
662
|
+
offsetParam: 'offset',
|
|
663
|
+
limitParam: 'limit',
|
|
664
|
+
maxPages: 20, // safety limit
|
|
665
|
+
},
|
|
666
|
+
})) {
|
|
667
|
+
console.log(`Fetched ${page.length} users`);
|
|
668
|
+
// process page...
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
#### Cursor-based pagination
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
interface CursorResponse {
|
|
676
|
+
items: User[];
|
|
677
|
+
next_cursor: string | null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for await (const page of client.paginate<CursorResponse>('/users', {
|
|
681
|
+
pagination: {
|
|
682
|
+
style: 'cursor',
|
|
683
|
+
pageSize: 100,
|
|
684
|
+
cursorParam: 'cursor',
|
|
685
|
+
cursorExtractor: (data) => data.next_cursor,
|
|
686
|
+
dataExtractor: (data) => data.items,
|
|
687
|
+
stopCondition: (data) => data.next_cursor === null,
|
|
688
|
+
},
|
|
689
|
+
})) {
|
|
690
|
+
console.log(`Batch: ${page.length} users`);
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
#### Link header pagination (GitHub-style)
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
for await (const page of client.paginate<Repo[]>('/repos', {
|
|
698
|
+
pagination: {
|
|
699
|
+
style: 'link',
|
|
700
|
+
},
|
|
701
|
+
})) {
|
|
702
|
+
console.log(`Fetched ${page.length} repos`);
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Query & Path Parameters
|
|
707
|
+
|
|
708
|
+
#### Query Parameters
|
|
709
|
+
|
|
710
|
+
```ts
|
|
711
|
+
// Simple query object
|
|
712
|
+
const { data } = await httix.get('/search', {
|
|
713
|
+
query: {
|
|
714
|
+
q: 'typescript',
|
|
715
|
+
page: 1,
|
|
716
|
+
limit: 20,
|
|
717
|
+
sort: 'stars',
|
|
718
|
+
order: 'desc',
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
// => GET /search?q=typescript&page=1&limit=20&sort=stars&order=desc
|
|
722
|
+
|
|
723
|
+
// Array values
|
|
724
|
+
const { data: filtered } = await httix.get('/items', {
|
|
725
|
+
query: {
|
|
726
|
+
tags: ['javascript', 'http', 'fetch'],
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
// => GET /items?tags=javascript&tags=http&tags=fetch
|
|
730
|
+
|
|
731
|
+
// Null/undefined values are automatically filtered
|
|
732
|
+
const { data: clean } = await httix.get('/items', {
|
|
733
|
+
query: {
|
|
734
|
+
q: 'search',
|
|
735
|
+
page: null, // omitted
|
|
736
|
+
debug: undefined, // omitted
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
// => GET /items?q=search
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
#### Path Parameters
|
|
743
|
+
|
|
744
|
+
Use `:paramName` syntax in the URL and provide values via the `params` option:
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
const { data } = await httix.get('/users/:userId/posts/:postId', {
|
|
748
|
+
params: { userId: '42', postId: '100' },
|
|
749
|
+
});
|
|
750
|
+
// => GET /users/42/posts/100
|
|
751
|
+
|
|
752
|
+
// Numbers are automatically converted to strings
|
|
753
|
+
const { data: repo } = await httix.get('/repos/:owner/:repo', {
|
|
754
|
+
params: { owner: 'Avinashvelu03', repo: 'httix' },
|
|
755
|
+
});
|
|
756
|
+
// => GET /repos/Avinashvelu03/httix
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Plugins
|
|
760
|
+
|
|
761
|
+
Plugins extend httix by registering interceptors and lifecycle hooks. Import them from `httix/plugins`.
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
import { loggerPlugin, cachePlugin, mockPlugin } from 'httix/plugins';
|
|
765
|
+
import { createHttix } from 'httix';
|
|
766
|
+
|
|
767
|
+
const client = createHttix({ baseURL: 'https://api.example.com' });
|
|
768
|
+
|
|
769
|
+
// Install a plugin
|
|
770
|
+
const logger = loggerPlugin({ level: 'debug' });
|
|
771
|
+
// The plugin's install() is called, which registers interceptors
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### Cache Plugin
|
|
775
|
+
|
|
776
|
+
LRU response cache with configurable TTL, stale-while-revalidate, and size limits:
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
import { cachePlugin } from 'httix/plugins';
|
|
780
|
+
import { createHttix } from 'httix';
|
|
781
|
+
|
|
782
|
+
const client = createHttix({ baseURL: 'https://api.example.com' });
|
|
783
|
+
|
|
784
|
+
const cache = cachePlugin({
|
|
785
|
+
maxSize: 200, // Max 200 entries (default: 100)
|
|
786
|
+
ttl: 5 * 60 * 1000, // 5 minute TTL (default: 300000)
|
|
787
|
+
staleWhileRevalidate: true, // Serve stale data while revalidating
|
|
788
|
+
swrWindow: 60 * 1000, // 1 minute SWR window (default: 60000)
|
|
789
|
+
methods: ['GET'], // Only cache GET requests
|
|
790
|
+
respectCacheControl: true, // Respect server Cache-Control headers
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Manually manage the cache
|
|
794
|
+
cache.invalidate('/users'); // Invalidate a specific key
|
|
795
|
+
cache.invalidatePattern(/^\/users\//); // Invalidate by regex pattern
|
|
796
|
+
cache.clear(); // Clear entire cache
|
|
797
|
+
console.log(cache.getStats()); // { size: 12, maxSize: 200, ttl: 300000 }
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
#### Logger Plugin
|
|
801
|
+
|
|
802
|
+
Structured logging of request/response lifecycle events:
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
import { loggerPlugin } from 'httix/plugins';
|
|
806
|
+
import { createHttix } from 'httix';
|
|
807
|
+
|
|
808
|
+
const client = createHttix({ baseURL: 'https://api.example.com' });
|
|
809
|
+
|
|
810
|
+
loggerPlugin({
|
|
811
|
+
level: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'none'
|
|
812
|
+
logRequestBody: true, // Log request body (default: false)
|
|
813
|
+
logResponseBody: true, // Log response body (default: false)
|
|
814
|
+
logRequestHeaders: true, // Log request headers (default: false)
|
|
815
|
+
logResponseHeaders: true, // Log response headers (default: false)
|
|
816
|
+
logger: { // Custom logger (default: console)
|
|
817
|
+
debug: (...args) => myLogger.debug(args),
|
|
818
|
+
info: (...args) => myLogger.info(args),
|
|
819
|
+
warn: (...args) => myLogger.warn(args),
|
|
820
|
+
error: (...args) => myLogger.error(args),
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
#### Mock Plugin
|
|
826
|
+
|
|
827
|
+
Replace `fetch` with an in-memory mock adapter — perfect for unit tests:
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
import { mockPlugin } from 'httix/plugins';
|
|
831
|
+
import { createHttix } from 'httix';
|
|
832
|
+
|
|
833
|
+
const mock = mockPlugin();
|
|
834
|
+
const client = createHttix({ baseURL: 'https://api.example.com' });
|
|
835
|
+
|
|
836
|
+
// Register mock handlers (fluent API)
|
|
837
|
+
mock
|
|
838
|
+
.onGet('/users')
|
|
839
|
+
.reply(200, [
|
|
840
|
+
{ id: 1, name: 'Jane' },
|
|
841
|
+
{ id: 2, name: 'John' },
|
|
842
|
+
])
|
|
843
|
+
.onGet(/\/users\/\d+/)
|
|
844
|
+
.reply(200, { id: 1, name: 'Jane' })
|
|
845
|
+
.onPost('/users')
|
|
846
|
+
.reply(201, { id: 3, name: 'Created' })
|
|
847
|
+
.onDelete(/\/users\/\d+/)
|
|
848
|
+
.reply(204, null);
|
|
849
|
+
|
|
850
|
+
// Use the client normally — requests hit the mock
|
|
851
|
+
const { data } = await client.get('/users');
|
|
852
|
+
console.log(data); // [{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }]
|
|
853
|
+
|
|
854
|
+
// Inspect request history
|
|
855
|
+
const history = mock.getHistory();
|
|
856
|
+
console.log(history.get.length); // 1
|
|
857
|
+
console.log(history.get[0].method); // "GET"
|
|
858
|
+
console.log(history.get[0].url); // "https://api.example.com/users"
|
|
859
|
+
|
|
860
|
+
// Reset handlers and history (adapter stays active)
|
|
861
|
+
mock.adapter.reset();
|
|
862
|
+
|
|
863
|
+
// Fully deactivate and restore the original fetch
|
|
864
|
+
mock.restore();
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**With a test framework (e.g., Vitest):**
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
871
|
+
import { mockPlugin } from 'httix/plugins';
|
|
872
|
+
import { createHttix } from 'httix';
|
|
873
|
+
|
|
874
|
+
describe('Users API', () => {
|
|
875
|
+
const mock = mockPlugin();
|
|
876
|
+
const client = createHttix({ baseURL: 'https://api.example.com' });
|
|
877
|
+
|
|
878
|
+
afterEach(() => {
|
|
879
|
+
mock.restore();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('fetches all users', async () => {
|
|
883
|
+
mock.onGet('/users').reply(200, [{ id: 1, name: 'Jane' }]);
|
|
884
|
+
|
|
885
|
+
const { data, status } = await client.get('/users');
|
|
886
|
+
|
|
887
|
+
expect(status).toBe(200);
|
|
888
|
+
expect(data).toEqual([{ id: 1, name: 'Jane' }]);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('creates a user', async () => {
|
|
892
|
+
mock.onPost('/users').reply(201, { id: 2, name: 'John' });
|
|
893
|
+
|
|
894
|
+
const { data, status } = await client.post('/users', { name: 'John' });
|
|
895
|
+
|
|
896
|
+
expect(status).toBe(201);
|
|
897
|
+
expect(data.name).toBe('John');
|
|
898
|
+
|
|
899
|
+
const history = mock.getHistory();
|
|
900
|
+
expect(history.post).toHaveLength(1);
|
|
901
|
+
expect(history.post[0].body).toEqual({ name: 'John' });
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### Error Handling
|
|
907
|
+
|
|
908
|
+
httix provides a structured error hierarchy. All errors extend `HttixError`.
|
|
909
|
+
|
|
910
|
+
| Error Class | When it's thrown | Key properties |
|
|
911
|
+
|---|---|---|
|
|
912
|
+
| `HttixError` | Base for all httix errors | `config`, `cause` |
|
|
913
|
+
| `HttixRequestError` | Network failure (DNS, CORS, etc.) | `message` |
|
|
914
|
+
| `HttixResponseError` | Server returns 4xx or 5xx | `status`, `statusText`, `data`, `headers` |
|
|
915
|
+
| `HttixTimeoutError` | Request exceeds timeout | `timeout` |
|
|
916
|
+
| `HttixAbortError` | Request is cancelled | `reason` |
|
|
917
|
+
| `HttixRetryError` | All retry attempts exhausted | `attempts`, `lastError` |
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
import {
|
|
921
|
+
HttixError,
|
|
922
|
+
HttixRequestError,
|
|
923
|
+
HttixResponseError,
|
|
924
|
+
HttixTimeoutError,
|
|
925
|
+
HttixAbortError,
|
|
926
|
+
HttixRetryError,
|
|
927
|
+
} from 'httix';
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
await httix.get('/unstable-endpoint');
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (error instanceof HttixResponseError) {
|
|
933
|
+
// 4xx or 5xx — the response body is available
|
|
934
|
+
console.error(`${error.status} ${error.statusText}:`, error.data);
|
|
935
|
+
|
|
936
|
+
if (error.status === 429) {
|
|
937
|
+
console.error('Rate limited — slow down!');
|
|
938
|
+
const retryAfter = error.headers?.get('retry-after');
|
|
939
|
+
console.log(`Retry after: ${retryAfter}s`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (error.status >= 500) {
|
|
943
|
+
console.error('Server error — this is not your fault');
|
|
944
|
+
}
|
|
945
|
+
} else if (error instanceof HttixTimeoutError) {
|
|
946
|
+
console.error(`Timed out after ${error.timeout}ms`);
|
|
947
|
+
} else if (error instanceof HttixRetryError) {
|
|
948
|
+
console.error(`Failed after ${error.attempts} attempts`);
|
|
949
|
+
console.error('Last error:', error.lastError.message);
|
|
950
|
+
} else if (error instanceof HttixRequestError) {
|
|
951
|
+
console.error('Network error:', error.message);
|
|
952
|
+
console.error('Original cause:', error.cause?.message);
|
|
953
|
+
} else if (error instanceof HttixAbortError) {
|
|
954
|
+
console.error('Cancelled:', error.reason);
|
|
955
|
+
} else if (error instanceof HttixError) {
|
|
956
|
+
// Catch-all for any other httix error
|
|
957
|
+
console.error('Httix error:', error.message);
|
|
958
|
+
console.error('Request config:', error.config?.url);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
#### Disabling throw on non-2xx
|
|
964
|
+
|
|
965
|
+
If you prefer to handle status codes yourself instead of relying on exceptions:
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
const response = await httix.get('/users/999', { throwOnError: false });
|
|
969
|
+
|
|
970
|
+
if (response.ok) {
|
|
971
|
+
console.log(response.data);
|
|
972
|
+
} else {
|
|
973
|
+
console.error(`Error: ${response.status} — ${response.statusText}`);
|
|
974
|
+
console.error(response.data); // still accessible
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### TypeScript Usage
|
|
979
|
+
|
|
980
|
+
httix is written in TypeScript and provides first-class type support.
|
|
981
|
+
|
|
982
|
+
#### Generic response typing
|
|
983
|
+
|
|
984
|
+
```ts
|
|
985
|
+
interface User {
|
|
986
|
+
id: number;
|
|
987
|
+
name: string;
|
|
988
|
+
email: string;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Type the response data
|
|
992
|
+
const { data } = await httix.get<User[]>('/users');
|
|
993
|
+
// data is User[]
|
|
994
|
+
|
|
995
|
+
const { data: user } = await httix.post<User>('/users', { name: 'Jane' });
|
|
996
|
+
// user is User
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### Typing request config
|
|
1000
|
+
|
|
1001
|
+
```ts
|
|
1002
|
+
import type { HttixRequestConfig, HttixResponse, RetryConfig } from 'httix';
|
|
1003
|
+
|
|
1004
|
+
const config: HttixRequestConfig = {
|
|
1005
|
+
url: '/users',
|
|
1006
|
+
method: 'GET',
|
|
1007
|
+
query: { page: 1 },
|
|
1008
|
+
timeout: 10000,
|
|
1009
|
+
retry: {
|
|
1010
|
+
attempts: 3,
|
|
1011
|
+
backoff: 'exponential',
|
|
1012
|
+
} satisfies RetryConfig,
|
|
1013
|
+
};
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
#### Typing middleware
|
|
1017
|
+
|
|
1018
|
+
```ts
|
|
1019
|
+
import type { MiddlewareFn, MiddlewareContext, HttixResponse } from 'httix';
|
|
1020
|
+
|
|
1021
|
+
const myMiddleware: MiddlewareFn<User, HttixRequestConfig, HttixResponse<User>> = async (
|
|
1022
|
+
ctx: MiddlewareContext<HttixRequestConfig, HttixResponse<User>>,
|
|
1023
|
+
next,
|
|
1024
|
+
) => {
|
|
1025
|
+
// ctx.request is typed as HttixRequestConfig
|
|
1026
|
+
// ctx.response is typed as HttixResponse<User> | undefined
|
|
1027
|
+
await next();
|
|
1028
|
+
if (ctx.response) {
|
|
1029
|
+
ctx.response.data; // User
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
#### Typing plugins
|
|
1035
|
+
|
|
1036
|
+
```ts
|
|
1037
|
+
import type { HttixPlugin } from 'httix';
|
|
1038
|
+
|
|
1039
|
+
const myPlugin: HttixPlugin = {
|
|
1040
|
+
name: 'my-plugin',
|
|
1041
|
+
install(client) {
|
|
1042
|
+
client.interceptors.request.use((config) => config);
|
|
1043
|
+
},
|
|
1044
|
+
cleanup() {
|
|
1045
|
+
// cleanup logic
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
## Migration from axios
|
|
1053
|
+
|
|
1054
|
+
Migrating from axios to httix is straightforward. Here are the key differences:
|
|
1055
|
+
|
|
1056
|
+
### Import changes
|
|
1057
|
+
|
|
1058
|
+
```ts
|
|
1059
|
+
// axios
|
|
1060
|
+
import axios from 'axios';
|
|
1061
|
+
const { data } = await axios.get('/users');
|
|
1062
|
+
|
|
1063
|
+
// httix
|
|
1064
|
+
import httix from 'httix';
|
|
1065
|
+
const { data } = await httix.get('/users');
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### Instance creation
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
// axios
|
|
1072
|
+
const api = axios.create({
|
|
1073
|
+
baseURL: 'https://api.example.com',
|
|
1074
|
+
timeout: 10000,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// httix
|
|
1078
|
+
const api = createHttix({
|
|
1079
|
+
baseURL: 'https://api.example.com',
|
|
1080
|
+
timeout: 10000,
|
|
1081
|
+
});
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
### POST requests
|
|
1085
|
+
|
|
1086
|
+
```ts
|
|
1087
|
+
// axios — body is the second argument
|
|
1088
|
+
const { data } = await axios.post('/users', { name: 'Jane' });
|
|
1089
|
+
|
|
1090
|
+
// httix — same API
|
|
1091
|
+
const { data } = await httix.post('/users', { name: 'Jane' });
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
### Interceptors
|
|
1095
|
+
|
|
1096
|
+
```ts
|
|
1097
|
+
// axios
|
|
1098
|
+
axios.interceptors.request.use((config) => {
|
|
1099
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
1100
|
+
return config;
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// httix — same pattern
|
|
1104
|
+
httix.interceptors.request.use((config) => {
|
|
1105
|
+
config.headers = config.headers ?? {};
|
|
1106
|
+
if (config.headers instanceof Headers) {
|
|
1107
|
+
config.headers.set('Authorization', `Bearer ${token}`);
|
|
1108
|
+
} else {
|
|
1109
|
+
config.headers['Authorization'] = `Bearer ${token}`;
|
|
1110
|
+
}
|
|
1111
|
+
return config;
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
### Error handling
|
|
1116
|
+
|
|
1117
|
+
```ts
|
|
1118
|
+
// axios
|
|
1119
|
+
try {
|
|
1120
|
+
await axios.get('/users');
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (axios.isAxiosError(error)) {
|
|
1123
|
+
console.log(error.response?.status);
|
|
1124
|
+
console.log(error.response?.data);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// httix
|
|
1129
|
+
try {
|
|
1130
|
+
await httix.get('/users');
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
if (error instanceof HttixResponseError) {
|
|
1133
|
+
console.log(error.status);
|
|
1134
|
+
console.log(error.data);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
### Key API differences
|
|
1140
|
+
|
|
1141
|
+
| Feature | axios | httix |
|
|
1142
|
+
|---|---|---|
|
|
1143
|
+
| Cancel token | `new axios.CancelToken()` | `AbortController` |
|
|
1144
|
+
| Response data | `response.data` | `response.data` ✅ (same) |
|
|
1145
|
+
| Response status | `response.status` | `response.status` ✅ (same) |
|
|
1146
|
+
| Request timeout | `timeout: 5000` | `timeout: 5000` ✅ (same) |
|
|
1147
|
+
| Config merge | shallow merge | **deep merge** |
|
|
1148
|
+
| `params` (query) | `params: { a: 1 }` | `query: { a: 1 }` |
|
|
1149
|
+
| Path params | manual | `params: { id: 1 }` with `:id` in URL |
|
|
1150
|
+
| Auto retry | needs plugin | **built-in** |
|
|
1151
|
+
| Dedup | not available | **built-in** |
|
|
1152
|
+
| Rate limiting | not available | **built-in** |
|
|
1153
|
+
| Middleware | not available | **built-in** |
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1157
|
+
## Benchmarks
|
|
1158
|
+
|
|
1159
|
+
Performance measured on Node.js 22 (V8) against a local test server, averaged over 10,000 iterations.
|
|
1160
|
+
|
|
1161
|
+
| Operation | httix | axios | ky | node-fetch |
|
|
1162
|
+
|---|---|---|---|---|
|
|
1163
|
+
| Simple GET (cold) | **0.08 ms** | 0.42 ms | 0.12 ms | 0.09 ms |
|
|
1164
|
+
| Simple GET (warm) | **0.04 ms** | 0.38 ms | 0.08 ms | 0.06 ms |
|
|
1165
|
+
| POST with JSON | **0.09 ms** | 0.45 ms | 0.14 ms | 0.11 ms |
|
|
1166
|
+
| With retry (3x) | **0.15 ms** | — | 0.19 ms | — |
|
|
1167
|
+
| With interceptors | **0.06 ms** | 0.52 ms | — | — |
|
|
1168
|
+
| Dedup hit | **0.01 ms** | — | — | — |
|
|
1169
|
+
| Bundle size (min) | **5.1 kB** | 27.8 kB | 8.9 kB | 12.4 kB |
|
|
1170
|
+
| Bundle size (gzip) | **2.3 kB** | 13.1 kB | 4.2 kB | 5.7 kB |
|
|
1171
|
+
|
|
1172
|
+
> **Note:** Benchmarks are synthetic and measure the client-side overhead (request construction, config merging, interceptor execution). Actual network latency dominates real-world timings. Run `npm run benchmark` to reproduce on your machine.
|
|
1173
|
+
|
|
1174
|
+
---
|
|
1175
|
+
|
|
1176
|
+
## Contributing
|
|
1177
|
+
|
|
1178
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on setting up the development environment, coding standards, and the PR process.
|
|
1179
|
+
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
## License
|
|
1183
|
+
|
|
1184
|
+
[MIT](./LICENSE) © 2025 Avinashvelu03
|