@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.
- package/README.md +1260 -0
- package/fetchify.js +227 -0
- 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
|
+
---
|