@rutansh0101/fetchify 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.
Files changed (3) hide show
  1. package/README.md +1260 -0
  2. package/fetchify.js +227 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,1260 @@
1
+ # Fetchify - A Lightweight HTTP Client Library
2
+
3
+ Fetchify is a modern, lightweight HTTP client library built on top of the native Fetch API. It provides a clean, axios-like interface with support for interceptors, request/response transformation, timeout handling, and more.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Installation & Setup](#installation--setup)
10
+ 2. [Basic Usage](#basic-usage)
11
+ 3. [Features Overview](#features-overview)
12
+ 4. [Detailed Feature Explanation](#detailed-feature-explanation)
13
+ - [Instance Creation](#1-instance-creation)
14
+ - [HTTP Methods](#2-http-methods)
15
+ - [Configuration Management](#3-configuration-management)
16
+ - [Timeout Handling](#4-timeout-handling)
17
+ - [Request Interceptors](#5-request-interceptors)
18
+ - [Response Interceptors](#6-response-interceptors)
19
+ - [Interceptor Chain Execution](#7-interceptor-chain-execution)
20
+ 5. [Architecture & Design Decisions](#architecture--design-decisions)
21
+ 6. [Code Examples](#code-examples)
22
+ 7. [Error Handling](#error-handling)
23
+ 8. [API Reference](#api-reference)
24
+
25
+ ---
26
+
27
+ ## Installation & Setup
28
+
29
+ ### Prerequisites
30
+ - Node.js environment with ES6+ module support
31
+ - Modern browser with Fetch API support
32
+
33
+ ### Setup
34
+ 1. Clone or download the project
35
+ 2. Ensure your project supports ES6 modules (add `"type": "module"` in package.json)
36
+ 3. Import Fetchify in your code:
37
+
38
+ ```javascript
39
+ import fetchify from "./fetchify.js";
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Basic Usage
45
+
46
+ ### Creating an Instance
47
+
48
+ ```javascript
49
+ import fetchify from "./fetchify.js";
50
+
51
+ // Create an instance with base configuration
52
+ const api = fetchify.create({
53
+ baseURL: 'https://api.example.com',
54
+ timeout: 5000,
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'Authorization': 'Bearer YOUR_TOKEN'
58
+ }
59
+ });
60
+ ```
61
+
62
+ ### Making Requests
63
+
64
+ ```javascript
65
+ // GET request
66
+ const response = await api.get('/users');
67
+ const users = await response.json();
68
+
69
+ // POST request
70
+ const newUser = await api.post('/users', {
71
+ body: JSON.stringify({ name: 'John', email: 'john@example.com' })
72
+ });
73
+
74
+ // PUT request
75
+ const updated = await api.put('/users/1', {
76
+ body: JSON.stringify({ name: 'John Updated' })
77
+ });
78
+
79
+ // PATCH request
80
+ const patched = await api.patch('/users/1', {
81
+ body: JSON.stringify({ email: 'newemail@example.com' })
82
+ });
83
+
84
+ // DELETE request
85
+ await api.delete('/users/1');
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Features Overview
91
+
92
+ ✅ **Axios-like API** - Familiar interface for developers coming from Axios
93
+ ✅ **Request/Response Interceptors** - Transform requests and responses globally
94
+ ✅ **Timeout Support** - Abort requests after specified duration
95
+ ✅ **Configuration Merging** - Instance, request-level, and default configs
96
+ ✅ **All HTTP Methods** - GET, POST, PUT, PATCH, DELETE support
97
+ ✅ **AbortController Integration** - Native request cancellation
98
+ ✅ **Promise Chain Architecture** - Clean async operation handling
99
+ ✅ **Error Transformation** - Custom error messages for timeouts and failures
100
+
101
+ ---
102
+
103
+ ## Detailed Feature Explanation
104
+
105
+ ### 1. Instance Creation
106
+
107
+ **What it does:**
108
+ Creates a reusable HTTP client instance with shared configuration.
109
+
110
+ **Why it's needed:**
111
+ - Avoid repeating baseURL, headers, and timeout in every request
112
+ - Create multiple instances for different APIs (e.g., one for auth, one for data)
113
+ - Centralize common configuration
114
+
115
+ **How it works:**
116
+
117
+ ```javascript
118
+ const create = (config) => {
119
+ return new Fetchify(config);
120
+ }
121
+ ```
122
+
123
+ The `create` method:
124
+ 1. Accepts a configuration object
125
+ 2. Instantiates the Fetchify class
126
+ 3. Merges user config with default config
127
+ 4. Returns the configured instance
128
+
129
+ **Implementation Details:**
130
+
131
+ ```javascript
132
+ constructor(newConfig) {
133
+ this.config = this.#mergeConfig(newConfig);
134
+ }
135
+ ```
136
+
137
+ The constructor:
138
+ - Calls `#mergeConfig` to combine default config with user-provided config
139
+ - Stores the merged config in `this.config`
140
+ - This config becomes the base for all future requests
141
+
142
+ **Default Configuration:**
143
+
144
+ ```javascript
145
+ config = {
146
+ headers: {
147
+ 'Content-Type': 'application/json'
148
+ },
149
+ timeout: 1000 // 1 second default timeout
150
+ };
151
+ ```
152
+
153
+ ---
154
+
155
+ ### 2. HTTP Methods
156
+
157
+ **What it does:**
158
+ Provides convenient methods for different HTTP verbs (GET, POST, PUT, PATCH, DELETE).
159
+
160
+ **Why it's needed:**
161
+ - Simplifies request syntax
162
+ - Makes code more readable (`api.get()` vs `api.request({ method: 'GET' })`)
163
+ - Follows REST conventions
164
+
165
+ **How it works:**
166
+
167
+ ```javascript
168
+ async get(endPoint, tempConfig = {}) {
169
+ return this.#request({
170
+ endPoint,
171
+ config: { ...tempConfig, method: 'GET' }
172
+ });
173
+ }
174
+ ```
175
+
176
+ Each method:
177
+ 1. Takes an endpoint and optional temporary config
178
+ 2. Merges the HTTP method into the config
179
+ 3. Calls the private `#request` method
180
+ 4. Returns a Promise that resolves to the fetch Response object
181
+
182
+ **All Methods:**
183
+
184
+ - **GET**: Retrieve data (no body allowed by HTTP spec)
185
+ - **POST**: Create new resources (requires body)
186
+ - **PUT**: Update entire resources (requires body)
187
+ - **PATCH**: Partially update resources (requires body)
188
+ - **DELETE**: Remove resources (usually no body)
189
+
190
+ **Example with Body:**
191
+
192
+ ```javascript
193
+ await api.post('/users', {
194
+ body: JSON.stringify({ name: 'John' }),
195
+ headers: { 'Custom-Header': 'value' }
196
+ });
197
+ ```
198
+
199
+ ---
200
+
201
+ ### 3. Configuration Management
202
+
203
+ **What it does:**
204
+ Handles merging of configurations at three levels: default, instance, and request.
205
+
206
+ **Why it's needed:**
207
+ - Allow global defaults that can be overridden
208
+ - Support instance-specific configs (different baseURLs)
209
+ - Enable request-specific overrides (custom timeout for one request)
210
+
211
+ **Configuration Priority (highest to lowest):**
212
+ 1. Request-level config (passed to `get()`, `post()`, etc.)
213
+ 2. Instance-level config (passed to `create()`)
214
+ 3. Default config (hardcoded in the class)
215
+
216
+ **How it works:**
217
+
218
+ ```javascript
219
+ #mergeConfig(newConfig) {
220
+ return {
221
+ ...this.config, // Existing config
222
+ ...newConfig, // New config (overrides existing)
223
+ headers: { // Deep merge for headers
224
+ ...this.config.headers,
225
+ ...newConfig?.headers
226
+ }
227
+ };
228
+ }
229
+ ```
230
+
231
+ **Why Deep Merge for Headers?**
232
+
233
+ Without deep merge:
234
+ ```javascript
235
+ // Instance config
236
+ headers: { 'Authorization': 'Bearer token', 'Content-Type': 'application/json' }
237
+
238
+ // Request config
239
+ headers: { 'X-Custom': 'value' }
240
+
241
+ // Result WITHOUT deep merge (WRONG):
242
+ headers: { 'X-Custom': 'value' } // Lost Authorization!
243
+
244
+ // Result WITH deep merge (CORRECT):
245
+ headers: {
246
+ 'Authorization': 'Bearer token',
247
+ 'Content-Type': 'application/json',
248
+ 'X-Custom': 'value'
249
+ }
250
+ ```
251
+
252
+ **Two Merge Methods:**
253
+
254
+ 1. `#mergeConfig(newConfig)` - Merges with instance config (`this.config`)
255
+ 2. `#mergeConfigs(config1, config2)` - Merges two arbitrary configs
256
+
257
+ ---
258
+
259
+ ### 4. Timeout Handling
260
+
261
+ **What it does:**
262
+ Automatically aborts requests that take longer than specified duration.
263
+
264
+ **Why it's needed:**
265
+ - Prevent requests from hanging indefinitely
266
+ - Improve user experience with faster feedback
267
+ - Handle slow/unresponsive servers gracefully
268
+ - Free up resources from stalled connections
269
+
270
+ **How it works:**
271
+
272
+ ```javascript
273
+ // 1. Create AbortController
274
+ const abortController = new AbortController();
275
+ const timeout = config.timeout || this.config.timeout || 0;
276
+
277
+ // 2. Set timer to abort
278
+ let timeOutId;
279
+ if (timeout) {
280
+ timeOutId = setTimeout(() => {
281
+ abortController.abort(); // Trigger abort after timeout
282
+ }, timeout);
283
+ }
284
+
285
+ // 3. Link signal to fetch
286
+ config.signal = abortController.signal;
287
+
288
+ // 4. Execute fetch
289
+ const response = await fetch(url, config);
290
+
291
+ // 5. Clear timer in finally block
292
+ finally {
293
+ if (timeOutId) {
294
+ clearTimeout(timeOutId); // Prevent memory leak
295
+ }
296
+ }
297
+ ```
298
+
299
+ **Why Use AbortController?**
300
+
301
+ - Native browser API for cancelling fetch requests
302
+ - Cleaner than old XMLHttpRequest cancellation
303
+ - Works across all modern browsers
304
+ - Can abort multiple operations with one controller
305
+
306
+ **Timeout Flow:**
307
+
308
+ ```
309
+ Request Start
310
+
311
+ Set Timeout Timer (e.g., 5000ms)
312
+
313
+ Start Fetch Request
314
+
315
+ ├─→ Response arrives in 2000ms → Clear Timer → Return Response ✓
316
+
317
+ └─→ 5000ms passes → Timer fires → Abort Signal → Fetch throws AbortError ✗
318
+ ```
319
+
320
+ **Why Clear Timeout?**
321
+
322
+ ```javascript
323
+ // Without clearing:
324
+ setTimeout(() => abortController.abort(), 5000);
325
+ // If request finishes in 2s, timer still exists in memory until 5s
326
+ // With 1000 requests, you have 1000 timers lingering!
327
+
328
+ // With clearing:
329
+ finally {
330
+ clearTimeout(timeOutId); // Immediately free memory when done
331
+ }
332
+ ```
333
+
334
+ **Error Handling:**
335
+
336
+ ```javascript
337
+ catch (error) {
338
+ if (error.name === 'AbortError') {
339
+ throw new Error(`Request to ${endPoint} aborted due to timeout after ${timeout} ms`);
340
+ } else {
341
+ throw error;
342
+ }
343
+ }
344
+ ```
345
+
346
+ The code checks `error.name === 'AbortError'` to distinguish timeout aborts from other errors.
347
+
348
+ ---
349
+
350
+ ### 5. Request Interceptors
351
+
352
+ **What it does:**
353
+ Intercepts and modifies requests before they are sent to the server.
354
+
355
+ **Why it's needed:**
356
+ - Add authentication tokens to all requests
357
+ - Log outgoing requests for debugging
358
+ - Modify request data (transform, encrypt, compress)
359
+ - Add timestamps or request IDs
360
+ - Implement retry logic
361
+
362
+ **How it works:**
363
+
364
+ ```javascript
365
+ // Adding an interceptor
366
+ api.addRequestInterceptor(
367
+ (config) => {
368
+ // Success handler: runs for every request
369
+ console.log('Sending request to:', config.endPoint);
370
+ config.config.headers['X-Request-Time'] = Date.now();
371
+ return config; // Must return config
372
+ },
373
+ (error) => {
374
+ // Error handler: runs if previous interceptor failed
375
+ console.error('Request interceptor error:', error);
376
+ return Promise.reject(error);
377
+ }
378
+ );
379
+ ```
380
+
381
+ **Storage:**
382
+
383
+ ```javascript
384
+ requestInterceptors = []; // Array of { successHandler, errorHandler }
385
+
386
+ addRequestInterceptor(successHandler, errorHandler) {
387
+ this.requestInterceptors.push({ successHandler, errorHandler });
388
+ }
389
+ ```
390
+
391
+ **Execution in Chain:**
392
+
393
+ ```javascript
394
+ const chain = [
395
+ ...this.requestInterceptors, // All request interceptors
396
+ { successHandler: this.#dispatchRequest.bind(this) }, // Actual fetch
397
+ ...this.responseInterceptors // All response interceptors
398
+ ];
399
+ ```
400
+
401
+ Request interceptors run **before** the actual fetch call.
402
+
403
+ **Input/Output Format:**
404
+
405
+ - **Input**: `{ endPoint: '/users', config: { method: 'GET', ... } }`
406
+ - **Output**: Must return same format (modified or not)
407
+
408
+ **Common Use Cases:**
409
+
410
+ ```javascript
411
+ // 1. Add authentication
412
+ api.addRequestInterceptor((config) => {
413
+ const token = localStorage.getItem('token');
414
+ config.config.headers['Authorization'] = `Bearer ${token}`;
415
+ return config;
416
+ });
417
+
418
+ // 2. Log requests
419
+ api.addRequestInterceptor((config) => {
420
+ console.log(`[${config.config.method}] ${config.endPoint}`);
421
+ return config;
422
+ });
423
+
424
+ // 3. Transform request body
425
+ api.addRequestInterceptor((config) => {
426
+ if (config.config.body) {
427
+ // Add timestamp to all POST requests
428
+ const body = JSON.parse(config.config.body);
429
+ body.timestamp = Date.now();
430
+ config.config.body = JSON.stringify(body);
431
+ }
432
+ return config;
433
+ });
434
+ ```
435
+
436
+ ---
437
+
438
+ ### 6. Response Interceptors
439
+
440
+ **What it does:**
441
+ Intercepts and modifies responses before they reach your application code.
442
+
443
+ **Why it's needed:**
444
+ - Transform response data globally
445
+ - Handle errors consistently (e.g., redirect on 401)
446
+ - Log responses for debugging
447
+ - Extract data from nested response structures
448
+ - Implement global retry logic
449
+
450
+ **How it works:**
451
+
452
+ ```javascript
453
+ // Adding an interceptor
454
+ api.addResponseInterceptor(
455
+ (response) => {
456
+ // Success handler: runs for successful responses
457
+ console.log('Response status:', response.status);
458
+
459
+ if (!response.ok) {
460
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
461
+ }
462
+
463
+ return response; // Must return response
464
+ },
465
+ (error) => {
466
+ // Error handler: runs on fetch errors or if previous interceptor threw
467
+ console.error('Response error:', error);
468
+
469
+ if (error.message.includes('timeout')) {
470
+ alert('Request timed out. Please try again.');
471
+ }
472
+
473
+ return Promise.reject(error);
474
+ }
475
+ );
476
+ ```
477
+
478
+ **Storage:**
479
+
480
+ ```javascript
481
+ responseInterceptors = []; // Array of { successHandler, errorHandler }
482
+
483
+ addResponseInterceptor(successHandler, errorHandler) {
484
+ this.responseInterceptors.push({ successHandler, errorHandler });
485
+ }
486
+ ```
487
+
488
+ **Execution in Chain:**
489
+
490
+ Response interceptors run **after** the fetch completes.
491
+
492
+ **Input/Output Format:**
493
+
494
+ - **Input**: Native fetch `Response` object
495
+ - **Output**: Must return `Response` object (or modified version)
496
+
497
+ **Common Use Cases:**
498
+
499
+ ```javascript
500
+ // 1. Auto-logout on 401
501
+ api.addResponseInterceptor(
502
+ (response) => {
503
+ if (response.status === 401) {
504
+ localStorage.removeItem('token');
505
+ window.location.href = '/login';
506
+ }
507
+ return response;
508
+ }
509
+ );
510
+
511
+ // 2. Parse all responses as JSON
512
+ api.addResponseInterceptor(
513
+ async (response) => {
514
+ const data = await response.json();
515
+ // Return modified response with parsed data
516
+ response.data = data;
517
+ return response;
518
+ }
519
+ );
520
+
521
+ // 3. Handle rate limiting
522
+ api.addResponseInterceptor(
523
+ (response) => {
524
+ if (response.status === 429) {
525
+ const retryAfter = response.headers.get('Retry-After');
526
+ throw new Error(`Rate limited. Retry after ${retryAfter} seconds`);
527
+ }
528
+ return response;
529
+ }
530
+ );
531
+
532
+ // 4. Log all responses
533
+ api.addResponseInterceptor(
534
+ (response) => {
535
+ console.log(`[${response.status}] Response received`);
536
+ return response;
537
+ }
538
+ );
539
+ ```
540
+
541
+ ---
542
+
543
+ ### 7. Interceptor Chain Execution
544
+
545
+ **What it does:**
546
+ Executes all interceptors and the fetch request in a sequential promise chain.
547
+
548
+ **Why it's needed:**
549
+ - Ensure interceptors run in order (request → fetch → response)
550
+ - Handle errors at any stage
551
+ - Allow each interceptor to modify data for the next
552
+ - Maintain clean async flow
553
+
554
+ **The Chain Structure:**
555
+
556
+ ```javascript
557
+ const chain = [
558
+ ...this.requestInterceptors, // [interceptor1, interceptor2, ...]
559
+ { successHandler: this.#dispatchRequest.bind(this) }, // The actual fetch
560
+ ...this.responseInterceptors // [interceptor3, interceptor4, ...]
561
+ ];
562
+ ```
563
+
564
+ **Visual Representation:**
565
+
566
+ ```
567
+ Initial Promise.resolve({ endPoint, config })
568
+
569
+ Request Interceptor 1 (success/error)
570
+
571
+ Request Interceptor 2 (success/error)
572
+
573
+ #dispatchRequest (actual fetch)
574
+
575
+ Response Interceptor 1 (success/error)
576
+
577
+ Response Interceptor 2 (success/error)
578
+
579
+ Final Result returned to user
580
+ ```
581
+
582
+ **Chain Building Code:**
583
+
584
+ ```javascript
585
+ let promise = Promise.resolve({ endPoint, config: finalConfig });
586
+
587
+ for (const { successHandler, errorHandler } of chain) {
588
+ promise = promise.then(
589
+ (responseOfPrevPromise) => {
590
+ try {
591
+ return successHandler(responseOfPrevPromise);
592
+ } catch (error) {
593
+ if (errorHandler) {
594
+ return errorHandler(error);
595
+ } else {
596
+ return Promise.reject(error);
597
+ }
598
+ }
599
+ },
600
+ (error) => {
601
+ if (errorHandler) {
602
+ return errorHandler(error);
603
+ } else {
604
+ return Promise.reject(error);
605
+ }
606
+ }
607
+ );
608
+ }
609
+
610
+ return promise;
611
+ ```
612
+
613
+ **How It Works Step-by-Step:**
614
+
615
+ 1. **Initial Promise**: Starts with `{ endPoint, config }`
616
+
617
+ 2. **Iteration**: For each interceptor in the chain:
618
+ - Attach `.then()` to the promise
619
+ - Pass output of previous step as input to next
620
+
621
+ 3. **Success Path**: `then(successHandler, errorHandler)`
622
+ - If previous promise resolved → `successHandler` runs
623
+ - Output becomes input for next interceptor
624
+
625
+ 4. **Error Path**: Second parameter of `.then()`
626
+ - If previous promise rejected → `errorHandler` runs
627
+ - Can recover (return value) or propagate (reject)
628
+
629
+ 5. **Try-Catch in Success Handler**:
630
+ - If `successHandler` throws synchronous error
631
+ - Catch it and pass to `errorHandler`
632
+ - This handles errors that don't return rejected promises
633
+
634
+ **Example Flow:**
635
+
636
+ ```javascript
637
+ // User code
638
+ const response = await api.get('/users');
639
+
640
+ // What happens:
641
+
642
+ // Step 1: Initial promise
643
+ Promise.resolve({ endPoint: '/users', config: { method: 'GET' } })
644
+
645
+ // Step 2: Request Interceptor 1
646
+ .then(config => {
647
+ config.config.headers['Auth'] = 'token';
648
+ return config; // { endPoint: '/users', config: { method: 'GET', headers: {...} } }
649
+ })
650
+
651
+ // Step 3: Request Interceptor 2
652
+ .then(config => {
653
+ console.log('Logging request');
654
+ return config; // Same object passed through
655
+ })
656
+
657
+ // Step 4: Dispatch Request (actual fetch)
658
+ .then(async config => {
659
+ const response = await fetch(config.config.baseURL + config.endPoint, config.config);
660
+ return response; // Native Response object
661
+ })
662
+
663
+ // Step 5: Response Interceptor 1
664
+ .then(response => {
665
+ if (!response.ok) throw new Error('Bad response');
666
+ return response; // Response object passed through
667
+ })
668
+
669
+ // Step 6: Response Interceptor 2
670
+ .then(response => {
671
+ console.log('Response received:', response.status);
672
+ return response; // Final Response object
673
+ })
674
+
675
+ // Result: User gets the Response object
676
+ ```
677
+
678
+ **Error Handling Example:**
679
+
680
+ ```javascript
681
+ // If Request Interceptor throws:
682
+ .then(config => {
683
+ throw new Error('Invalid token'); // Sync error
684
+ })
685
+
686
+ // Caught by try-catch, passed to errorHandler:
687
+ catch (error) {
688
+ if (errorHandler) {
689
+ return errorHandler(error); // Can recover
690
+ } else {
691
+ return Promise.reject(error); // Propagate
692
+ }
693
+ }
694
+
695
+ // If fetch fails (network error, timeout):
696
+ .then(async config => {
697
+ const response = await fetch(...); // Throws on timeout
698
+ return response;
699
+ })
700
+
701
+ // Second parameter of .then() catches it:
702
+ .then(successHandler, (error) => {
703
+ if (errorHandler) {
704
+ return errorHandler(error);
705
+ } else {
706
+ return Promise.reject(error);
707
+ }
708
+ })
709
+ ```
710
+
711
+ **Why Use This Pattern?**
712
+
713
+ 1. **Sequential Execution**: Each interceptor waits for previous one
714
+ 2. **Error Recovery**: Interceptors can catch and fix errors
715
+ 3. **Immutable Chain**: Original promise isn't modified
716
+ 4. **Type Safety**: Each step expects specific input/output format
717
+ 5. **Debugging**: Easy to add console.logs at each step
718
+
719
+ **Key Points:**
720
+
721
+ - Interceptors must return a value (config or response)
722
+ - Errors can be caught and handled at any point
723
+ - The chain is built once per request
724
+ - All interceptors share the same promise chain
725
+ - `bind(this)` ensures `#dispatchRequest` maintains class context
726
+
727
+ ---
728
+
729
+ ## Architecture & Design Decisions
730
+
731
+ ### Why Classes?
732
+
733
+ - Encapsulation of state (config, interceptors)
734
+ - Private methods (#) for internal logic
735
+ - Instance creation for multiple API clients
736
+ - Clear separation of concerns
737
+
738
+ ### Why Private Methods (#)?
739
+
740
+ ```javascript
741
+ #request()
742
+ #dispatchRequest()
743
+ #mergeConfig()
744
+ #mergeConfigs()
745
+ ```
746
+
747
+ - Hide implementation details
748
+ - Prevent external modification
749
+ - Clear API surface (only public methods exposed)
750
+ - Follows encapsulation principles
751
+
752
+ ### Why Async/Await in #dispatchRequest?
753
+
754
+ ```javascript
755
+ // Way 1: Promise chain
756
+ return fetch(url, config).finally(() => clearTimeout(timeOutId));
757
+
758
+ // Way 2: Async/Await (chosen)
759
+ try {
760
+ const response = await fetch(url, config);
761
+ return response;
762
+ } finally {
763
+ clearTimeout(timeOutId);
764
+ }
765
+ ```
766
+
767
+ **Chosen Way 2 because:**
768
+ - More readable for error handling
769
+ - Finally block guaranteed to run
770
+ - Easier to debug with stack traces
771
+ - Synchronous-looking async code
772
+
773
+ ### Why Bind Context?
774
+
775
+ ```javascript
776
+ successHandler: this.#dispatchRequest.bind(this)
777
+ ```
778
+
779
+ Without `bind(this)`:
780
+ ```javascript
781
+ // Inside interceptor chain loop
782
+ successHandler(config); // 'this' is undefined in #dispatchRequest!
783
+ ```
784
+
785
+ With `bind(this)`:
786
+ ```javascript
787
+ // 'this' correctly refers to Fetchify instance
788
+ this.config, this.requestInterceptors, etc. are accessible
789
+ ```
790
+
791
+ ### Why Two Merge Methods?
792
+
793
+ 1. **#mergeConfig(newConfig)**: Merges with `this.config`
794
+ - Used in constructor and #request
795
+ - Instance-specific merging
796
+
797
+ 2. **#mergeConfigs(config1, config2)**: Merges two arbitrary configs
798
+ - Used in #dispatchRequest
799
+ - Doesn't depend on instance state
800
+
801
+ Separation provides flexibility and reusability.
802
+
803
+ ---
804
+
805
+ ## Code Examples
806
+
807
+ ### Complete Example with All Features
808
+
809
+ ```javascript
810
+ import fetchify from "./fetchify.js";
811
+
812
+ // 1. Create instance with base config
813
+ const api = fetchify.create({
814
+ baseURL: 'https://api.example.com',
815
+ timeout: 5000,
816
+ headers: {
817
+ 'Content-Type': 'application/json',
818
+ 'X-API-Version': 'v1'
819
+ }
820
+ });
821
+
822
+ // 2. Add request interceptor for auth
823
+ api.addRequestInterceptor(
824
+ (config) => {
825
+ const token = localStorage.getItem('authToken');
826
+ if (token) {
827
+ config.config.headers['Authorization'] = `Bearer ${token}`;
828
+ }
829
+ console.log('→ Request:', config.config.method, config.endPoint);
830
+ return config;
831
+ },
832
+ (error) => {
833
+ console.error('Request interceptor failed:', error);
834
+ return Promise.reject(error);
835
+ }
836
+ );
837
+
838
+ // 3. Add response interceptor for error handling
839
+ api.addResponseInterceptor(
840
+ (response) => {
841
+ console.log('← Response:', response.status, response.statusText);
842
+
843
+ if (!response.ok) {
844
+ if (response.status === 401) {
845
+ localStorage.removeItem('authToken');
846
+ window.location.href = '/login';
847
+ }
848
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
849
+ }
850
+
851
+ return response;
852
+ },
853
+ (error) => {
854
+ if (error.message.includes('timeout')) {
855
+ alert('Request timed out. Please check your connection.');
856
+ }
857
+ return Promise.reject(error);
858
+ }
859
+ );
860
+
861
+ // 4. Make requests
862
+ async function example() {
863
+ try {
864
+ // GET request
865
+ const usersResponse = await api.get('/users');
866
+ const users = await usersResponse.json();
867
+ console.log('Users:', users);
868
+
869
+ // POST request with custom timeout
870
+ const newUser = await api.post('/users', {
871
+ body: JSON.stringify({
872
+ name: 'John Doe',
873
+ email: 'john@example.com'
874
+ }),
875
+ timeout: 10000 // Override default timeout
876
+ });
877
+ const created = await newUser.json();
878
+ console.log('Created user:', created);
879
+
880
+ // PUT request
881
+ const updateResponse = await api.put('/users/1', {
882
+ body: JSON.stringify({ name: 'Jane Doe' })
883
+ });
884
+
885
+ // PATCH request
886
+ const patchResponse = await api.patch('/users/1', {
887
+ body: JSON.stringify({ email: 'jane@example.com' })
888
+ });
889
+
890
+ // DELETE request
891
+ await api.delete('/users/1');
892
+
893
+ } catch (error) {
894
+ console.error('Request failed:', error.message);
895
+ }
896
+ }
897
+
898
+ example();
899
+ ```
900
+
901
+ ### Multiple API Instances
902
+
903
+ ```javascript
904
+ // Different APIs with different configs
905
+ const mainAPI = fetchify.create({
906
+ baseURL: 'https://api.example.com',
907
+ timeout: 5000
908
+ });
909
+
910
+ const authAPI = fetchify.create({
911
+ baseURL: 'https://auth.example.com',
912
+ timeout: 10000
913
+ });
914
+
915
+ const analyticsAPI = fetchify.create({
916
+ baseURL: 'https://analytics.example.com',
917
+ timeout: 3000,
918
+ headers: {
919
+ 'X-Analytics-Key': 'secret123'
920
+ }
921
+ });
922
+
923
+ // Use them independently
924
+ await mainAPI.get('/users');
925
+ await authAPI.post('/login', { body: credentials });
926
+ await analyticsAPI.post('/track', { body: event });
927
+ ```
928
+
929
+ ---
930
+
931
+ ## Error Handling
932
+
933
+ ### Types of Errors
934
+
935
+ 1. **Network Errors**: No internet, DNS failure, etc.
936
+ 2. **Timeout Errors**: Request exceeded timeout duration
937
+ 3. **HTTP Errors**: 4xx, 5xx status codes
938
+ 4. **Interceptor Errors**: Thrown by custom interceptor logic
939
+
940
+ ### Error Handling Pattern
941
+
942
+ ```javascript
943
+ async function safeRequest() {
944
+ try {
945
+ const response = await api.get('/users', { timeout: 2000 });
946
+
947
+ // Check HTTP status
948
+ if (!response.ok) {
949
+ throw new Error(`HTTP ${response.status}`);
950
+ }
951
+
952
+ const data = await response.json();
953
+ return data;
954
+
955
+ } catch (error) {
956
+ // Timeout error
957
+ if (error.message.includes('timeout')) {
958
+ console.error('Request timed out');
959
+ return null;
960
+ }
961
+
962
+ // Network error
963
+ if (error.message.includes('fetch')) {
964
+ console.error('Network error');
965
+ return null;
966
+ }
967
+
968
+ // Other errors
969
+ console.error('Unknown error:', error);
970
+ throw error;
971
+ }
972
+ }
973
+ ```
974
+
975
+ ### Global Error Handling with Interceptors
976
+
977
+ ```javascript
978
+ api.addResponseInterceptor(
979
+ (response) => {
980
+ // Handle specific status codes globally
981
+ switch(response.status) {
982
+ case 401:
983
+ console.error('Unauthorized - redirecting to login');
984
+ window.location.href = '/login';
985
+ break;
986
+ case 403:
987
+ console.error('Forbidden - insufficient permissions');
988
+ break;
989
+ case 404:
990
+ console.error('Resource not found');
991
+ break;
992
+ case 500:
993
+ console.error('Server error');
994
+ break;
995
+ }
996
+ return response;
997
+ },
998
+ (error) => {
999
+ // Log all errors globally
1000
+ console.error('[Global Error Handler]', error);
1001
+
1002
+ // Send error to monitoring service
1003
+ // sendToSentry(error);
1004
+
1005
+ return Promise.reject(error);
1006
+ }
1007
+ );
1008
+ ```
1009
+
1010
+ ---
1011
+
1012
+ ## API Reference
1013
+
1014
+ ### fetchify.create(config)
1015
+
1016
+ Creates a new Fetchify instance.
1017
+
1018
+ **Parameters:**
1019
+ - `config` (Object): Configuration object
1020
+ - `baseURL` (String): Base URL for all requests
1021
+ - `timeout` (Number): Default timeout in milliseconds
1022
+ - `headers` (Object): Default headers for all requests
1023
+
1024
+ **Returns:** Fetchify instance
1025
+
1026
+ **Example:**
1027
+ ```javascript
1028
+ const api = fetchify.create({
1029
+ baseURL: 'https://api.example.com',
1030
+ timeout: 5000,
1031
+ headers: { 'Authorization': 'Bearer token' }
1032
+ });
1033
+ ```
1034
+
1035
+ ---
1036
+
1037
+ ### instance.get(endpoint, config)
1038
+
1039
+ Performs a GET request.
1040
+
1041
+ **Parameters:**
1042
+ - `endpoint` (String): API endpoint (appended to baseURL)
1043
+ - `config` (Object, optional): Request-specific config
1044
+ - `timeout` (Number): Override default timeout
1045
+ - `headers` (Object): Additional headers
1046
+ - Any other fetch options
1047
+
1048
+ **Returns:** Promise<Response>
1049
+
1050
+ **Example:**
1051
+ ```javascript
1052
+ const response = await api.get('/users', { timeout: 3000 });
1053
+ const users = await response.json();
1054
+ ```
1055
+
1056
+ ---
1057
+
1058
+ ### instance.post(endpoint, config)
1059
+
1060
+ Performs a POST request.
1061
+
1062
+ **Parameters:**
1063
+ - `endpoint` (String): API endpoint
1064
+ - `config` (Object, optional): Request-specific config
1065
+ - `body` (String): Request body (usually JSON.stringify)
1066
+ - `timeout` (Number): Override default timeout
1067
+ - `headers` (Object): Additional headers
1068
+
1069
+ **Returns:** Promise<Response>
1070
+
1071
+ **Example:**
1072
+ ```javascript
1073
+ const response = await api.post('/users', {
1074
+ body: JSON.stringify({ name: 'John' }),
1075
+ timeout: 10000
1076
+ });
1077
+ ```
1078
+
1079
+ ---
1080
+
1081
+ ### instance.put(endpoint, config)
1082
+
1083
+ Performs a PUT request (full update).
1084
+
1085
+ **Parameters:** Same as POST
1086
+
1087
+ **Example:**
1088
+ ```javascript
1089
+ await api.put('/users/1', {
1090
+ body: JSON.stringify({ name: 'John', email: 'john@example.com' })
1091
+ });
1092
+ ```
1093
+
1094
+ ---
1095
+
1096
+ ### instance.patch(endpoint, config)
1097
+
1098
+ Performs a PATCH request (partial update).
1099
+
1100
+ **Parameters:** Same as POST
1101
+
1102
+ **Example:**
1103
+ ```javascript
1104
+ await api.patch('/users/1', {
1105
+ body: JSON.stringify({ email: 'newemail@example.com' })
1106
+ });
1107
+ ```
1108
+
1109
+ ---
1110
+
1111
+ ### instance.delete(endpoint, config)
1112
+
1113
+ Performs a DELETE request.
1114
+
1115
+ **Parameters:**
1116
+ - `endpoint` (String): API endpoint
1117
+ - `config` (Object, optional): Request-specific config
1118
+
1119
+ **Returns:** Promise<Response>
1120
+
1121
+ **Example:**
1122
+ ```javascript
1123
+ await api.delete('/users/1');
1124
+ ```
1125
+
1126
+ ---
1127
+
1128
+ ### instance.addRequestInterceptor(successHandler, errorHandler)
1129
+
1130
+ Adds a request interceptor.
1131
+
1132
+ **Parameters:**
1133
+ - `successHandler` (Function): Called before request
1134
+ - Receives: `{ endPoint, config }`
1135
+ - Must return: Modified `{ endPoint, config }`
1136
+ - `errorHandler` (Function, optional): Called if previous interceptor failed
1137
+ - Receives: Error object
1138
+ - Must return: Rejected promise or recovery value
1139
+
1140
+ **Example:**
1141
+ ```javascript
1142
+ api.addRequestInterceptor(
1143
+ (config) => {
1144
+ config.config.headers['X-Timestamp'] = Date.now();
1145
+ return config;
1146
+ },
1147
+ (error) => {
1148
+ console.error('Request error:', error);
1149
+ return Promise.reject(error);
1150
+ }
1151
+ );
1152
+ ```
1153
+
1154
+ ---
1155
+
1156
+ ### instance.addResponseInterceptor(successHandler, errorHandler)
1157
+
1158
+ Adds a response interceptor.
1159
+
1160
+ **Parameters:**
1161
+ - `successHandler` (Function): Called after response received
1162
+ - Receives: Response object
1163
+ - Must return: Response object (modified or not)
1164
+ - `errorHandler` (Function, optional): Called on request failure
1165
+ - Receives: Error object
1166
+ - Must return: Rejected promise or recovery value
1167
+
1168
+ **Example:**
1169
+ ```javascript
1170
+ api.addResponseInterceptor(
1171
+ (response) => {
1172
+ console.log('Status:', response.status);
1173
+ return response;
1174
+ },
1175
+ (error) => {
1176
+ if (error.message.includes('timeout')) {
1177
+ alert('Request timed out');
1178
+ }
1179
+ return Promise.reject(error);
1180
+ }
1181
+ );
1182
+ ```
1183
+
1184
+ ---
1185
+
1186
+ ## Comparison with Axios
1187
+
1188
+ | Feature | Fetchify | Axios |
1189
+ |---------|----------|-------|
1190
+ | Size | ~5KB | ~20KB |
1191
+ | Dependencies | None (native fetch) | Standalone library |
1192
+ | Browser Support | Modern browsers | All browsers (polyfills) |
1193
+ | Interceptors | ✅ | ✅ |
1194
+ | Timeout | ✅ | ✅ |
1195
+ | Request Cancellation | ✅ (AbortController) | ✅ (CancelToken) |
1196
+ | Automatic JSON Transform | ❌ (manual) | ✅ |
1197
+ | Progress Events | ❌ | ✅ |
1198
+ | TypeScript | ❌ | ✅ |
1199
+
1200
+ ---
1201
+
1202
+ ## Future Enhancements
1203
+
1204
+ Potential features to add:
1205
+ - Retry logic with exponential backoff
1206
+ - Request caching
1207
+ - Automatic JSON parsing
1208
+ - Progress events for uploads
1209
+ - TypeScript definitions
1210
+ - Request/response transformation helpers
1211
+ - CSRF token handling
1212
+ - File upload helpers
1213
+
1214
+ ---
1215
+
1216
+ ## License
1217
+
1218
+ MIT License - Free to use and modify
1219
+
1220
+ ---
1221
+
1222
+ ## Contributing
1223
+
1224
+ Feel free to submit issues and pull requests for improvements!
1225
+
1226
+ ---
1227
+
1228
+ **Last Updated:** January 2026
1229
+ **Version:** 1.0.0
1230
+ **Author:** Rutansh Chawla
1231
+
1232
+ ---
1233
+
1234
+ ## Quick Reference Card
1235
+
1236
+ ```javascript
1237
+ // Create instance
1238
+ const api = fetchify.create({ baseURL: '...', timeout: 5000 });
1239
+
1240
+ // Requests
1241
+ await api.get('/path', { timeout: 3000 });
1242
+ await api.post('/path', { body: JSON.stringify(data) });
1243
+ await api.put('/path', { body: JSON.stringify(data) });
1244
+ await api.patch('/path', { body: JSON.stringify(data) });
1245
+ await api.delete('/path');
1246
+
1247
+ // Interceptors
1248
+ api.addRequestInterceptor((config) => { /* modify */ return config; });
1249
+ api.addResponseInterceptor((response) => { /* handle */ return response; });
1250
+
1251
+ // Error handling
1252
+ try {
1253
+ const res = await api.get('/path');
1254
+ const data = await res.json();
1255
+ } catch (error) {
1256
+ console.error(error.message);
1257
+ }
1258
+ ```
1259
+
1260
+ ---