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/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) &copy; 2025 Avinashvelu03