@autofleet/rapido-http-client 0.0.1

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,780 @@
1
+ # @autofleet/rapido-http-client
2
+
3
+ > [!TIP]
4
+ > TL;DR - Modern, high-performance HTTP client built on [undici](https://github.com/nodejs/undici) - Node.js's official modern HTTP client.
5
+
6
+ **Rapido HTTP Client** is a high-performance, type-safe HTTP client for Node.js applications, built on top of [undici](https://github.com/nodejs/undici). It offers advanced features like automatic retries, caching, schema validation with Zod, and seamless integration with service discovery systems.
7
+
8
+ ## Why Rapido HTTP Client?
9
+
10
+ Rapido HTTP Client is the next-generation replacement for `@autofleet/network`, designed from the ground up for modern Node.js applications:
11
+ ### Key Advantages Over @autofleet/network
12
+
13
+ | Feature | Rapido HTTP Client (undici) | Network (axios) |
14
+ |---------|-------------------|-----------------|
15
+ | **Performance** | ✅ Undici by Node.js, nearly 200x faster | ❌ Axios is not optimized for speed |
16
+ | **Modern APIs** | ✅ Built-in caching, retry interceptors | ❌ Requires plugins |
17
+ | **Type Safety** | ✅ Full TypeScript + Opt-in Zod validation | ⚠️ Only type casting (with generics) |
18
+ | **Memory Efficiency** | ✅ Connection pooling | ⚠️ No pooling, slower connections |
19
+ | **Maintenance** | ✅ Official Node.js project | ⚠️ Unmaintained version of Axios (0.x) |
20
+
21
+ ### Performance Benchmarks
22
+
23
+ Benchmarks comparing Rapido HTTP Client (undici) vs Network (axios):
24
+
25
+ | Operation | Rapido HTTP Client (ops/sec) | Network (ops/sec) | **Performance Gain** |
26
+ |-----------|---------------------|-------------------|----------------------|
27
+ | Initialization | 1,173,118 | 60,947 | **19.2x faster** ⚡ |
28
+ | Simple GET | 61,916 | 319 | **194x faster** ⚡ |
29
+ | POST with body | 61,459 | 334 | **184x faster** ⚡ |
30
+ | GET with params | 58,347 | 270 | **216x faster** ⚡ |
31
+ | PUT request | 64,213 | 349 | **184x faster** ⚡ |
32
+ | DELETE request | 111,402 | 624 | **178x faster** ⚡ |
33
+ | With Zod schema | 58,860 | 289 | **204x faster** ⚡ |
34
+ | getAllPages | 20,654 | 119 | **173x faster** ⚡ |
35
+ | With retries | 827 | 146 | **5.7x faster** ⚡ |
36
+
37
+ **Rapido HTTP Client is 170-200x faster** than Network for most operations, especially in happy-paths!
38
+
39
+ Take into account that these benchmarks are run in ideal conditions - no actual network latency. In real-world scenarios, the performance gap may be even larger due to Rapido HTTP Client's efficient connection pooling and lower overhead.
40
+ It might also become smaller in high-latency networks, where network time dominates processing time.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm i @autofleet/rapido-http-client
46
+ ```
47
+
48
+ ### Optional Dependencies
49
+
50
+ For schema validation support:
51
+ ```bash
52
+ npm i zod
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ <details>
58
+ <summary><b>Basic Usage</b></summary>
59
+
60
+ ```typescript
61
+ import RapidoHttpClient from '@autofleet/rapido-http-client';
62
+ import { logger } from '@autofleet/logger';
63
+
64
+ const client = new RapidoHttpClient({
65
+ logger,
66
+ serviceUrl: 'https://api.example.com',
67
+ });
68
+
69
+ // Simple GET request
70
+ const { data, status } = await client.get('/users');
71
+
72
+ // POST with body
73
+ const response = await client.post('/users', {
74
+ name: 'John Doe',
75
+ email: 'john@example.com',
76
+ });
77
+
78
+ // Clean up when done
79
+ await client.close();
80
+ ```
81
+
82
+ </details>
83
+
84
+ <details>
85
+ <summary><b>With Service Discovery (Kubernetes)</b></summary>
86
+
87
+ ```typescript
88
+ const client = new RapidoHttpClient({
89
+ logger,
90
+ serviceName: 'USER', // Reads from USER_SERVICE_HOST env var
91
+ });
92
+
93
+ const { data } = await client.get('/profile');
94
+ ```
95
+
96
+ </details>
97
+
98
+ <details>
99
+ <summary><b>With Retry Logic</b></summary>
100
+
101
+ ```typescript
102
+ const client = new RapidoHttpClient({
103
+ serviceUrl: 'https://api.example.com',
104
+ logger,
105
+ retries: {
106
+ maxRetries: 3,
107
+ minTimeout: 100,
108
+ timeoutFactor: 2, // Exponential backoff
109
+ statusCodes: [429, 500, 502, 503], // Retry on these status codes
110
+ },
111
+ });
112
+
113
+ // Automatically retries on failure
114
+ const { data } = await client.get('/flaky-endpoint');
115
+ ```
116
+
117
+ </details>
118
+
119
+ <details>
120
+ <summary><b>With HTTP Caching</b></summary>
121
+
122
+ ```typescript
123
+ const client = new RapidoHttpClient({
124
+ serviceUrl: 'https://api.example.com',
125
+ logger,
126
+ cache: {
127
+ maxCount: 1000, // Max cached responses
128
+ maxSize: 10 * 1024 * 1024, // 10MB cache size
129
+ },
130
+ });
131
+
132
+ // First request hits network
133
+ const response1 = await client.get('/data');
134
+
135
+ // Second request returns from cache (if cache-control headers permit)
136
+ const response2 = await client.get('/data');
137
+ ```
138
+
139
+ </details>
140
+
141
+ <details>
142
+ <summary><b>With Zod Schema Validation</b></summary>
143
+
144
+ ```typescript
145
+ import { z } from 'zod';
146
+
147
+ const UserSchema = z.object({
148
+ id: z.number(),
149
+ name: z.string(),
150
+ email: z.string().email(),
151
+ role: z.enum(['admin', 'user']),
152
+ });
153
+
154
+ // Type is automatically inferred from schema!
155
+ const { data } = await client.get('/user/123', {
156
+ responseSchema: UserSchema,
157
+ });
158
+
159
+ // TypeScript knows: data is { id: number; name: string; email: string; role: 'admin' | 'user' }
160
+ console.log(data.name); // ✅ Type-safe & Runtime-validated!
161
+
162
+ // Throws ZodError if response doesn't match schema
163
+ ```
164
+
165
+ </details>
166
+
167
+ <details>
168
+ <summary><b>Request Timeouts & Cancellation</b></summary>
169
+
170
+ ```typescript
171
+ // Global timeout for all requests
172
+ const client = new RapidoHttpClient({
173
+ logger,
174
+ timeout: 5_000, // 5 seconds
175
+ serviceUrl: 'https://api.example.com',
176
+ });
177
+
178
+ // Override timeout per request
179
+ await client.get('/slow-endpoint', { timeout: 10_000 });
180
+
181
+ // Manual cancellation with AbortSignal
182
+ try {
183
+ await client.get('/data', { signal: AbortSignal.timeout(3_000) });
184
+ } catch (error) {
185
+ // RequestAbortedError thrown when cancelled
186
+ }
187
+ ```
188
+
189
+ </details>
190
+
191
+ <details>
192
+ <summary><b>Query Parameters</b></summary>
193
+
194
+ ```typescript
195
+ // Simple params
196
+ await client.get('/search', {
197
+ params: { q: 'nodejs', limit: 10 },
198
+ });
199
+ // → GET /search?q=nodejs&limit=10
200
+
201
+ // Array params (auto-formatted with [])
202
+ await client.get('/filter', {
203
+ params: { tags: ['typescript', 'nodejs'] },
204
+ });
205
+ // → GET /filter?tags[]=typescript&tags[]=nodejs
206
+ ```
207
+
208
+ </details>
209
+
210
+ <details>
211
+ <summary><b>Custom Headers & Outbreak Tracing</b></summary>
212
+
213
+ ```typescript
214
+ const client = new RapidoHttpClient({
215
+ serviceUrl: 'https://api.example.com',
216
+ logger,
217
+ headers: {
218
+ 'X-API-Key': 'secret-key',
219
+ 'X-Custom-Header': 'value',
220
+ },
221
+ });
222
+
223
+ // Outbreak trace headers are automatically injected
224
+ // from the current context (x-trace-id, x-correlation-id, etc.)
225
+ await client.get('/data');
226
+
227
+ // Override headers per request
228
+ await client.get('/data', {
229
+ headers: { 'X-Request-Specific': 'value' },
230
+ });
231
+ ```
232
+
233
+ </details>
234
+
235
+ <details>
236
+ <summary><b>Pagination Helpers</b></summary>
237
+
238
+ ```typescript
239
+ // Fetch all pages from a paginated GET endpoint
240
+ const allItems = await client.getAllPages<Item>('/api/items');
241
+ // Automatically fetches pages until no more data
242
+
243
+ // Fetch all pages from a POST query endpoint
244
+ const allResults = await client.getAllPagesFromQueryEndpoint<Result>(
245
+ '/api/query',
246
+ { filter: 'active' }, // Additional query parameters
247
+ 100 // Max pages (default: 100, -1 for unlimited)
248
+ );
249
+ ```
250
+
251
+ </details>
252
+
253
+ <details>
254
+ <summary><b>Async Disposal (Node.js 20+)</b></summary>
255
+
256
+ ```typescript
257
+ // Automatic cleanup with async using
258
+ {
259
+ await using client = new RapidoHttpClient({
260
+ serviceUrl: 'https://api.example.com',
261
+ logger,
262
+ });
263
+
264
+ await client.get('/data');
265
+
266
+ // client.close() called automatically when scope exits
267
+ }
268
+ ```
269
+
270
+ </details>
271
+
272
+ <details>
273
+ <summary><b>Automatic Response Decompression</b></summary>
274
+
275
+ ```typescript
276
+ // Enable automatic decompression of gzip, brotli, deflate, and zstd responses
277
+ const client = new RapidoHttpClient({
278
+ logger,
279
+ autoDecompress: true,
280
+ serviceUrl: 'https://api.example.com',
281
+ });
282
+
283
+ // Automatically decompresses compressed responses
284
+ // Sets Accept-Encoding header to: zstd, br, gzip (or br, gzip if Node version is lacking zstd support)
285
+ const { data } = await client.get('/data');
286
+
287
+ // Works with both success and error responses
288
+ // No need to manually handle content-encoding headers
289
+ ```
290
+
291
+ </details>
292
+
293
+ ## Compression Support
294
+
295
+ Rapido HTTP Client supports automatic decompression of compressed HTTP responses through the `autoDecompress` option.
296
+
297
+ ### Enabling Auto-Decompression
298
+
299
+ ```typescript
300
+ const client = new RapidoHttpClient({
301
+ logger,
302
+ serviceUrl: 'https://api.example.com',
303
+ autoDecompress: true,
304
+ });
305
+ ```
306
+
307
+ ### What It Does
308
+
309
+ When `autoDecompress: true` is enabled:
310
+
311
+ 1. **Sets Accept-Encoding Header**: Automatically adds the `Accept-Encoding` header to advertise supported compression formats:
312
+ - **Node.js 22+**: `zstd, br, gzip` (includes zstd support)
313
+ - **Node.js < 22**: `br, gzip` (zstd not supported in older versions)
314
+
315
+ 2. **Automatic Decompression**: Transparently decompresses responses with the following `content-encoding` values:
316
+ - `gzip` - GZIP compression
317
+ - `br` - Brotli compression
318
+ - `deflate` - Deflate compression
319
+ - `zstd` - Zstandard compression (Node.js 22+ only)
320
+
321
+ 3. **Works for All Responses**: Handles both successful responses and error responses (4xx, 5xx)
322
+
323
+ ### Example Usage
324
+
325
+ ```typescript
326
+ const client = new RapidoHttpClient({
327
+ logger,
328
+ serviceUrl: 'https://api.example.com',
329
+ autoDecompress: true,
330
+ });
331
+
332
+ // Server responds with gzipped JSON and content-encoding: gzip
333
+ const { data } = await client.get('/data');
334
+
335
+ // Rapido HTTP Client automatically decompresses the response
336
+ // You receive the parsed JSON object directly
337
+ console.log(data); // { ... } - already decompressed and parsed
338
+ ```
339
+
340
+ ### When to Use
341
+
342
+ Enable `autoDecompress` when:
343
+ - Communicating with external APIs that send compressed responses
344
+ - Optimizing bandwidth usage with servers that support compression
345
+ - Working with CDNs or services that automatically compress responses
346
+
347
+ ### When Not to Use
348
+
349
+ Leave `autoDecompress` disabled (default) when:
350
+ - Internal microservice communication where responses aren't compressed (at least at the time of writing this)
351
+ - You need to handle raw compressed data yourself
352
+ - The overhead of checking and decompressing isn't worth the bandwidth savings
353
+
354
+ ### Error Handling
355
+
356
+ Decompression works transparently for error responses too:
357
+
358
+ ```typescript
359
+ try {
360
+ await client.get('/api/endpoint');
361
+ } catch (error) {
362
+ if (error instanceof errors.RapidoHttpClientError) {
363
+ // error.data is already decompressed, even if the server
364
+ // sent a compressed 4xx/5xx response
365
+ console.log(error.data);
366
+ }
367
+ }
368
+ ```
369
+
370
+ ## API Reference
371
+
372
+ ### HTTP Methods
373
+
374
+ All methods return `Promise<RapidoHttpClientResponse<T>>`:
375
+
376
+ - `get<T>(url, config?)` - GET request
377
+ - `post<T>(url, body?, config?)` - POST request
378
+ - `put<T>(url, body?, config?)` - PUT request
379
+ - `patch<T>(url, body?, config?)` - PATCH request
380
+ - `delete<T>(url, config?)` - DELETE request
381
+ - `head<T>(url, config?)` - HEAD request
382
+ - `options<T>(url, config?)` - OPTIONS request
383
+ - `query<T>(url, body?, config?)` - QUERY request
384
+
385
+ ### Request Config
386
+
387
+ ```typescript
388
+ interface RequestConfig {
389
+ params?: Record<string, any>; // Query parameters
390
+ headers?: Record<string, string>;// Request headers
391
+ timeout?: number; // Override timeout
392
+ signal?: AbortSignal; // Cancellation signal
393
+ responseSchema?: ZodType; // Response validation schema
394
+ }
395
+ ```
396
+
397
+ ### Response Format
398
+
399
+ ```typescript
400
+ interface RapidoHttpClientResponse<T> {
401
+ data: T; // Parsed response body
402
+ status: number; // HTTP status code
403
+ statusText: string; // Status text
404
+ headers: Record<string, string | string[]>; // Response headers
405
+ config: { // Request metadata
406
+ url: string;
407
+ method: string;
408
+ baseURL?: string;
409
+ };
410
+ }
411
+ ```
412
+
413
+ ## Testing
414
+
415
+ Rapido HTTP Client provides a simple, nock-like testing API that allows you to mock HTTP responses without having to change how you instantiate your client.
416
+
417
+ This mock API only mocks the network layer, so your Rapido HTTP Client instances are created normally - no special test parameters are needed, while all features (retries, caching, schema validation) work as expected.
418
+
419
+ ### Quick Start
420
+
421
+ ```typescript
422
+ import { setupRapidoHttpClientMock } from '@autofleet/rapido-http-client/testing';
423
+ import RapidoHttpClient from '@autofleet/rapido-http-client';
424
+
425
+ describe('My Service Tests', () => {
426
+ let mock: ReturnType<typeof setupRapidoHttpClientMock>;
427
+
428
+ beforeAll(() => {
429
+ // Set up the global mock
430
+ mock = setupRapidoHttpClientMock();
431
+ });
432
+
433
+ afterAll(async () => {
434
+ // Clean up after each test
435
+ await mock.cleanup();
436
+ });
437
+
438
+ it('should fetch users', async () => {
439
+ // Set up mock responses
440
+ mock.intercept('https://api.example.com')
441
+ .get('/users')
442
+ .reply(200, { users: [{ id: 1, name: 'John' }] }, {
443
+ headers: { 'content-type': 'application/json' }
444
+ });
445
+
446
+ // Create your client normally - no special parameters needed!
447
+ const client = new RapidoHttpClient({
448
+ serviceUrl: 'https://api.example.com',
449
+ logger: mockLogger,
450
+ });
451
+
452
+ // Make requests - they automatically use the mocks
453
+ const response = await client.get('/users');
454
+
455
+ expect(response.data).toEqual({ users: [{ id: 1, name: 'John' }] });
456
+ });
457
+ });
458
+ ```
459
+
460
+ ### API Reference
461
+
462
+ #### `setupRapidoHttpClientMock()`
463
+
464
+ Creates a global mock that is automatically used by all Rapido HTTP Client instances.
465
+
466
+ > [!IMPORTANT]
467
+ > The `setupRapidoHttpClientMock()` function must be called **before** creating the Rapido HTTP Client instance, so the instantiation picks up the global mock agent.
468
+ > This means you should typically call it in `beforeAll` hooks, and after that dynamically import the module creating the Rapido HTTP Client instance.
469
+
470
+ ##### `intercept(target)`
471
+
472
+ Set up mocks for a specific origin. Accepts either a URL string or an object with `serviceName` or `serviceUrl`. Returns an instance of unici's `MockInterceptor` object. See [the docs](https://undici.nodejs.org/#/docs/api/MockPool?id=return-mockinterceptor) for details.
473
+
474
+ ###### Some examples:
475
+
476
+ ```typescript
477
+ const mock = setupRapidoHttpClientMock();
478
+
479
+ // Simple path matching
480
+ mock.intercept('https://api.example.com')
481
+ .get('/users')
482
+ .reply(200, { users: [] }, { headers: { 'content-type': 'application/json' } });
483
+
484
+ // Advanced: Match specific request body
485
+ mock.intercept('https://api.example.com')
486
+ .post({ path: '/users', body: JSON.stringify({ name: 'Jane' }) })
487
+ .reply(201, { id: 2, name: 'Jane' }, { headers: { 'content-type': 'application/json' } });
488
+
489
+ // Advanced: Match query parameters
490
+ mock.intercept('https://api.example.com')
491
+ .get({ path: '/users', query: { page: '1', limit: '10' } })
492
+ .reply(200, { users: [], page: 1 }, { headers: { 'content-type': 'application/json' } });
493
+
494
+ // Using serviceName (reads from env variable)
495
+ mock.intercept({ serviceName: 'USER' })
496
+ .get('/profile')
497
+ .reply(200, { id: 1, name: 'Alice' }, { headers: { 'content-type': 'application/json' } });
498
+
499
+ // Using serviceUrl
500
+ mock.intercept({ serviceUrl: 'https://api.example.com' })
501
+ .delete('/users/1')
502
+ .reply(204);
503
+
504
+ // Mock for a specific number of requests
505
+ mock.intercept('https://api.example.com')
506
+ .get('/data')
507
+ .reply(200, { value: 1 }, { headers: { 'content-type': 'application/json' } })
508
+ .times(3); // Only mock the first 3 requests
509
+
510
+ // Mock indefinitely
511
+ mock.intercept('https://api.example.com')
512
+ .get('/data')
513
+ .reply(200, { value: 1 }, { headers: { 'content-type': 'application/json' } })
514
+ .persist(); // Always return this response
515
+
516
+ // Mock a network error
517
+ const error = new Error('ECONNREFUSED: Connection refused');
518
+ error.code = 'ECONNREFUSED';
519
+
520
+ mock.intercept('https://api.example.com')
521
+ .get('/failing-endpoint')
522
+ .replyWithError(error)
523
+ .persist(); // Always throw this error
524
+
525
+ // Mock timeout errors
526
+ const timeoutError = new Error('Request timeout');
527
+ timeoutError.code = 'UND_ERR_TIMEOUT';
528
+
529
+ mock.intercept('https://api.example.com')
530
+ .get('/slow-endpoint')
531
+ .replyWithError(timeoutError)
532
+ .times(2); // First 2 requests fail, then succeeds
533
+
534
+ const client = new RapidoHttpClient({
535
+ serviceUrl: 'https://api.example.com',
536
+ logger,
537
+ });
538
+
539
+ // This will throw the error
540
+ await expect(client.get('/failing-endpoint')).rejects.toThrow('ECONNREFUSED');
541
+ ```
542
+
543
+ ##### `getPool(origin: string): MockPool`
544
+
545
+ Get the underlying [undici `MockPool`](https://undici.nodejs.org/#/docs/api/MockPool?id=instance-methods) for advanced usage:
546
+
547
+ ```typescript
548
+ const pool = mock.getPool('https://api.example.com');
549
+
550
+ // Use undici's full API
551
+ pool.intercept({
552
+ path: '/complex',
553
+ method: 'GET',
554
+ headers: { 'x-custom': 'value' }
555
+ })
556
+ .reply(200, { advanced: true }, { headers: { 'content-type': 'application/json' } });
557
+ ```
558
+
559
+ ##### `cleanup(): Promise<void>`
560
+
561
+ Clean up the global mock. **Should be called** after tests complete (typically in `afterAll`):
562
+
563
+ ```typescript
564
+ afterAll(async () => {
565
+ await mock.cleanup();
566
+ });
567
+ ```
568
+
569
+ ### Testing Examples
570
+
571
+ <details>
572
+ <summary><b>Mocking Multiple Services</b></summary>
573
+
574
+ ```typescript
575
+ it('works with multiple services', async () => {
576
+ mock.intercept('https://api1.example.com')
577
+ .get('/data')
578
+ .reply(200, { service: 'api1' }, { headers: { 'content-type': 'application/json' } });
579
+
580
+ mock.intercept('https://api2.example.com')
581
+ .get('/data')
582
+ .reply(200, { service: 'api2' }, { headers: { 'content-type': 'application/json' } });
583
+
584
+ const client1 = new RapidoHttpClient({
585
+ serviceUrl: 'https://api1.example.com',
586
+ logger: mockLogger,
587
+ });
588
+
589
+ const client2 = new RapidoHttpClient({
590
+ serviceUrl: 'https://api2.example.com',
591
+ logger: mockLogger,
592
+ });
593
+
594
+ const response1 = await client1.get('/data');
595
+ const response2 = await client2.get('/data');
596
+
597
+ expect(response1.data).toEqual({ service: 'api1' });
598
+ expect(response2.data).toEqual({ service: 'api2' });
599
+ });
600
+ ```
601
+
602
+ </details>
603
+
604
+ <details>
605
+ <summary><b>Error Responses</b></summary>
606
+
607
+ ```typescript
608
+ it('handles error responses', async () => {
609
+ mock.intercept('https://api.example.com')
610
+ .get('/users/999')
611
+ .reply(404, { error: 'Not Found' }, { headers: { 'content-type': 'application/json' } });
612
+
613
+ const client = new RapidoHttpClient({
614
+ serviceUrl: 'https://api.example.com',
615
+ logger: mockLogger,
616
+ });
617
+
618
+ await expect(client.get('/users/999')).rejects.toThrow();
619
+ });
620
+ ```
621
+
622
+ </details>
623
+
624
+ <details>
625
+ <summary><b>Advanced `MockPool` Usage</b></summary>
626
+
627
+ For full control, use `getPool()` to access the undici `MockPool` directly:
628
+
629
+ ```typescript
630
+ it('uses advanced pool features', async () => {
631
+ const pool = mock.getPool('https://api.example.com');
632
+
633
+ pool.intercept({
634
+ path: '/data',
635
+ method: 'POST',
636
+ body: (body) => {
637
+ // Custom body matching logic
638
+ const data = JSON.parse(body as string);
639
+ return data.name === 'test';
640
+ },
641
+ headers: {
642
+ 'x-api-key': 'secret'
643
+ }
644
+ })
645
+ .reply(201, { created: true }, { headers: { 'content-type': 'application/json' } });
646
+
647
+ const client = new RapidoHttpClient({
648
+ serviceUrl: 'https://api.example.com',
649
+ logger: mockLogger,
650
+ headers: { 'x-api-key': 'secret' }
651
+ });
652
+
653
+ const response = await client.post('/data', { name: 'test' });
654
+ expect(response.status).toBe(201);
655
+ });
656
+ ```
657
+
658
+ </details>
659
+
660
+ ### How It Works
661
+
662
+ When you call `setupRapidoHttpClientMock()`, it creates a global MockAgent that is stored in `globalThis`. The Rapido HTTP Client constructor automatically checks for this global mock agent and uses it instead of creating a real HTTP client.
663
+
664
+ This means:
665
+ - ✅ No need to pass special test parameters to your Rapido HTTP Client instances
666
+ - ✅ Your production code remains unchanged
667
+ - ✅ Works with any testing framework (Vitest, Jest, etc.)
668
+ - ✅ Clean separation between test setup and implementation
669
+
670
+ ### Migration from nock
671
+
672
+ If you're migrating from `nock`, the API is very similar:
673
+
674
+ ```typescript
675
+ // Old (nock)
676
+ nock('https://api.example.com')
677
+ .get('/users')
678
+ .reply(200, { users: [] });
679
+
680
+ // New (rapido-http-client)
681
+ const mock = setupRapidoHttpClientMock();
682
+ mock.intercept('https://api.example.com')
683
+ .get('/users')
684
+ .reply(200, { users: [] }, { headers: { 'content-type': 'application/json' } });
685
+ ```
686
+
687
+ The main differences:
688
+ - You need to call `mock.cleanup()` in `afterAll`
689
+ - Headers should be specified in the third parameter to `reply()`
690
+ - The mock is set up once per test file/suite
691
+
692
+ ## Running Tests & Benchmarks
693
+
694
+ ### Run Tests
695
+
696
+ ```bash
697
+ # Run all tests
698
+ pnpm --filter @autofleet/rapido-http-client test
699
+ ```
700
+
701
+ ### Generate Coverage Report
702
+
703
+ ```bash
704
+ pnpm --filter @autofleet/rapido-http-client coverage
705
+ ```
706
+
707
+ ### Run Benchmarks
708
+
709
+ ```bash
710
+ # Run all benchmarks
711
+ pnpm --filter @autofleet/rapido-http-client bench
712
+
713
+ # Run benchmarks and save results
714
+ pnpm --filter @autofleet/rapido-http-client bench --outputJson benchmark-results.json
715
+
716
+ # Compare with previous results
717
+ pnpm --filter @autofleet/rapido-http-client bench --compare benchmark-results.json
718
+ ```
719
+
720
+ ## Migration from @autofleet/network
721
+
722
+ ### Basic Request
723
+
724
+ **Before (Network):**
725
+ ```typescript
726
+ import Network from '@autofleet/network';
727
+
728
+ const network = new Network({
729
+ serviceUrl: 'https://api.example.com',
730
+ logger,
731
+ });
732
+
733
+ const response = await network.get('/data');
734
+ const data = response.data;
735
+ ```
736
+
737
+ **After (Rapido HTTP Client):**
738
+ ```typescript
739
+ import { RapidoHttpClient } from '@autofleet/rapido-http-client';
740
+
741
+ const client = new RapidoHttpClient({
742
+ serviceUrl: 'https://api.example.com',
743
+ logger,
744
+ });
745
+
746
+ const { data } = await client.get('/data');
747
+ ```
748
+
749
+ ### With Retries
750
+
751
+ **Before (Network):**
752
+ ```typescript
753
+ const network = new Network({
754
+ serviceUrl: 'https://api.example.com',
755
+ logger,
756
+ retries: 3,
757
+ retryDelay: (retryCount) => retryCount * 1000,
758
+ });
759
+ ```
760
+
761
+ **After (Rapido HTTP Client):**
762
+ ```typescript
763
+ const client = new RapidoHttpClient({
764
+ serviceUrl: 'https://api.example.com',
765
+ logger,
766
+ retries: {
767
+ maxRetries: 3,
768
+ minTimeout: 1000,
769
+ timeoutFactor: 1,
770
+ },
771
+ });
772
+ ```
773
+
774
+ ### Key Differences
775
+
776
+ 1. **Default Type**: Rapido HTTP Client uses `unknown` instead of `any` for better type safety
777
+ 2. **Error Handling**: Uses undici's error classes (`ResponseError`, `RequestAbortedError`, `UndiciError`)
778
+ 3. **Cache**: Built-in HTTP caching support (no external plugin needed)
779
+ 4. **Schema Validation**: Native Zod support with type inference
780
+ 5. **Disposal**: Supports async disposal pattern (`await using`)