@scirexs/fetchy 0.6.1 → 0.7.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 CHANGED
@@ -8,18 +8,18 @@ A lightweight, type-safe fetch wrapper with built-in retry logic, timeout handli
8
8
 
9
9
  ## Features
10
10
 
11
- - **Lightweight** - Bundle size is ~6KB uncompressed, ~3KB gzipped, zero dependencies
11
+ - **Lightweight** - Bundle size is ~5KB uncompressed, ~2KB gzipped, zero dependencies
12
12
  - **Simple API** - Drop-in replacement for native fetch with enhanced capabilities
13
+ - **Promise-like Interface** - Chain parsing methods directly on fetch results
13
14
  - **Timeout Support** - Configurable request timeouts with automatic cancellation
14
15
  - **Retry Logic** - Exponential backoff with Retry-After header support
15
16
  - **Type-Safe** - Full TypeScript support with generic type inference
16
17
  - **Bearer Token Helper** - Built-in Authorization header management
17
18
  - **Jitter Support** - Prevent thundering herd with randomized delays
18
- - **Automatic Body Parsing** - Automatic JSON serialization and Content-Type detection
19
19
  - **Fluent Interface** - Class-based API with both instance and static methods
20
+ - **HTTP Method Shortcuts** - Convenient methods for GET, POST, PUT, PATCH, DELETE
20
21
 
21
22
  ## Installation
22
-
23
23
  ```bash
24
24
  # npm
25
25
  npm install @scirexs/fetchy
@@ -29,72 +29,69 @@ deno add jsr:@scirexs/fetchy
29
29
  ```
30
30
 
31
31
  ## Quick Start
32
-
33
32
  ```ts
34
- import { fetchy, sfetchy, Fetchy } from "@scirexs/fetchy";
33
+ import { fetchy, sfetchy, Fetchy, fy } from "@scirexs/fetchy";
35
34
 
36
- // Simple GET request with timeout and retry
37
- const response = await fetchy("https://api.example.com/data");
35
+ // Simple GET request with automatic JSON parsing
36
+ const user = await fetchy("https://api.example.com/user/1").json<User>();
38
37
 
39
- // Auto-parsed JSON response with safe error handling
40
- interface User {
41
- id: number;
42
- name: string;
38
+ // Safe error handling - returns null on failure
39
+ const data = await sfetchy("https://api.example.com/data").json<Data>();
40
+ if (data !== null) {
41
+ console.log(data);
43
42
  }
44
43
 
45
- const user = await sfetchy<User>("https://api.example.com/user/1", { timeout: 10 }, "json");
46
- console.log(user.name);
47
-
48
44
  // Fluent API with reusable configuration
49
- const client = new Fetchy({
45
+ const client = fy({
50
46
  bearer: "token",
51
47
  timeout: 10,
52
48
  retry: { maxAttempts: 5 }
53
49
  });
54
- const data = await client.json<User>("https://api.example.com/user/1");
50
+ const posts = await client.get("/posts").json<Post[]>();
55
51
  ```
56
52
 
57
53
  ## API Reference
58
54
 
59
- ### `fetchy(url, options?, parse?)`
55
+ ### `fetchy(url?, options?)`
60
56
 
61
- Performs an HTTP request with enhanced features. Throws errors on failure by default.
57
+ Performs an HTTP request with enhanced features. Returns a promise-like object that can be awaited directly or chained with parsing methods.
62
58
 
63
59
  #### Parameters
64
60
 
65
- - `url`: `string | URL | Request | null` - The request URL
61
+ - `url`: `string | URL | Request | null` - The request URL (can be null if `options.url` is provided)
66
62
  - `options`: `FetchyOptions` (optional) - Configuration options
67
- - `parse`: `"json" | "text" | "bytes" | "blob" | "buffer"` (optional) - Response parsing method
68
63
 
69
64
  #### Returns
70
65
 
71
- - Without `parse`: `Promise<Response>`
72
- - With `parse="json"`: `Promise<T>`
73
- - With `parse="text"`: `Promise<string>`
74
- - With `parse="bytes"`: `Promise<Uint8Array>`
75
- - With `parse="blob"`: `Promise<Blob>`
76
- - With `parse="buffer"`: `Promise<ArrayBuffer>`
66
+ `FetchyResponse` - A promise-like object that extends `Promise<Response>` with the following methods:
77
67
 
78
- #### Example
68
+ - `text()` → `Promise<string>` - Parse response as text
69
+ - `json<T>()` → `Promise<T>` - Parse response as JSON
70
+ - `bytes()` → `Promise<Uint8Array>` - Parse response as byte array
71
+ - `blob()` → `Promise<Blob>` - Parse response as Blob
72
+ - `arrayBuffer()` → `Promise<ArrayBuffer>` - Parse response as ArrayBuffer
73
+ - `formData()` → `Promise<FormData>` - Parse response as FormData
79
74
 
75
+ #### Example
80
76
  ```ts
81
77
  // Get Response object
82
78
  const response = await fetchy("https://api.example.com/data");
83
79
 
84
- // Direct JSON parsing
85
- const user = await fetchy<User>("https://api.example.com/user", {}, "json");
80
+ // Chain JSON parsing
81
+ const user = await fetchy("https://api.example.com/user").json<User>();
86
82
 
87
83
  // POST with automatic body serialization
88
84
  const result = await fetchy("https://api.example.com/create", {
85
+ method: "POST",
89
86
  body: { name: "John", age: 30 },
90
87
  bearer: "token"
91
- }, "json");
88
+ }).json();
92
89
 
93
90
  // Binary data
94
- const image = await fetchy("https://api.example.com/image.png", {}, "bytes");
91
+ const image = await fetchy("https://api.example.com/image.png").bytes();
95
92
  ```
96
93
 
97
- ### `sfetchy(url, options?, parse?)`
94
+ ### `sfetchy(url?, options?)`
98
95
 
99
96
  Performs an HTTP request with safe error handling. Returns `null` on any failure instead of throwing.
100
97
 
@@ -104,133 +101,174 @@ Same as `fetchy()`.
104
101
 
105
102
  #### Returns
106
103
 
107
- Same as `fetchy()` but with `| null` added to each return type.
104
+ `FetchySafeResponse` - A promise-like object that extends `Promise<Response | null>` with the same parsing methods as `FetchyResponse`.
108
105
 
109
- #### Example
106
+ - `text()` → `Promise<string | null>` - Safe text parsing (returns null on error)
107
+ - `json<T>()` → `Promise<T | null>` - Safe JSON parsing (returns null on error)
108
+ - `bytes()` → `Promise<Uint8Array | null>` - Safe bytes parsing
109
+ - `blob()` → `Promise<Blob | null>` - Safe blob parsing
110
+ - `arrayBuffer()` → `Promise<ArrayBuffer | null>` - Safe buffer parsing
111
+ - `formData()` → `Promise<FormData | null>` - Safe form data parsing
110
112
 
113
+ #### Example
111
114
  ```ts
112
- // Returns null instead of throwing
113
- const data = await sfetchy("https://api.example.com/data", {}, "json");
114
- if (data === null) {
115
+ // Returns null instead of throwing on error
116
+ const response = await sfetchy("https://api.example.com/data");
117
+ if (response === null) {
115
118
  console.log("Request failed gracefully");
119
+ } else {
120
+ const data = await response.json();
116
121
  }
117
122
 
118
- // Safe Response retrieval
119
- const response = await sfetchy("https://api.example.com/data");
120
- if (response?.ok) {
121
- const json = await response.json();
123
+ // Safe parsing still available
124
+ const data = await sfetchy("https://api.example.com/data").json<Data>();
125
+ if (data !== null) {
126
+ // Handle successful response
122
127
  }
123
128
  ```
124
129
 
125
130
  ### `Fetchy` Class
126
131
 
127
- A fluent HTTP client class that provides both instance and static methods.
132
+ A fluent HTTP client class that provides instance methods.
128
133
 
129
- #### Instance Methods
134
+ #### Constructor
135
+ ```ts
136
+ const client = new Fetchy(options?: FetchyOptions);
137
+ ```
130
138
 
139
+ #### Instance Methods
131
140
  ```ts
132
141
  const client = new Fetchy(options);
133
142
 
134
- // Parsing methods
135
- await client.fetch(url?) // Returns Response
136
- await client.json<T>(url?) // Returns T
137
- await client.text(url?) // Returns string
138
- await client.bytes(url?) // Returns Uint8Array
139
- await client.blob(url?) // Returns Blob
140
- await client.buffer(url?) // Returns ArrayBuffer
141
- await client.safe(url?) // Returns Response | null
142
- await client.sjson<T>(url?) // Returns T | null
143
- await client.stext(url?) // Returns string | null
144
- await client.sbytes(url?) // Returns Uint8Array | null
145
- await client.sblob(url?) // Returns Blob | null
146
- await client.sbuffer(url?) // Returns ArrayBuffer | null
143
+ // Main fetch method
144
+ await client.fetch(url?) // Returns FetchyResponse
145
+
146
+ // HTTP method shortcuts
147
+ await client.get(url?) // GET request, returns FetchyResponse
148
+ await client.post(url?) // POST request, returns FetchyResponse
149
+ await client.put(url?) // PUT request, returns FetchyResponse
150
+ await client.patch(url?) // PATCH request, returns FetchyResponse
151
+ await client.delete(url?) // DELETE request, returns FetchyResponse
152
+ await client.head(url?) // HEAD request, returns Promise<Response>
153
+
154
+ // Safe mode methods
155
+ await client.safe(url?) // Returns FetchySafeResponse
156
+ await client.sget(url?) // Safe GET, returns FetchySafeResponse
157
+ await client.spost(url?) // Safe POST, returns FetchySafeResponse
158
+ await client.sput(url?) // Safe PUT, returns FetchySafeResponse
159
+ await client.spatch(url?) // Safe PATCH, returns FetchySafeResponse
160
+ await client.sdelete(url?) // Safe DELETE, returns FetchySafeResponse
161
+ await client.shead(url?) // Safe HEAD, returns Promise<Response | null>
147
162
  ```
148
163
 
149
- #### Static Methods
150
-
164
+ All methods can be chained with parsing methods:
151
165
  ```ts
152
- // Same methods available as static
153
- await Fetchy.fetch(url, options?)
154
- await Fetchy.json<T>(url, options?)
155
- await Fetchy.text(url, options?)
156
- await Fetchy.bytes(url, options?)
157
- await Fetchy.blob(url, options?)
158
- await Fetchy.buffer(url, options?)
159
- await Fetchy.safe(url, options?)
160
- await Fetchy.sjson<T>(url, options?)
161
- await Fetchy.stext(url, options?)
162
- await Fetchy.sbytes(url, options?)
163
- await Fetchy.sblob(url, options?)
164
- await Fetchy.sbuffer(url, options?)
166
+ await client.get("/users").json<User[]>();
167
+ await client.post("/create").json<Result>();
168
+ await client.safe("/data").text();
165
169
  ```
166
170
 
167
171
  #### Example
168
-
169
172
  ```ts
170
173
  // Instance usage - reuse configuration
171
174
  const client = new Fetchy({
175
+ base: "https://api.example.com",
172
176
  bearer: "token123",
173
177
  timeout: 10,
174
178
  retry: { maxAttempts: 3 }
175
179
  });
176
180
 
177
- const user = await client.json<User>("https://api.example.com/user");
178
- const posts = await client.json<Post[]>("https://api.example.com/posts");
181
+ const user = await client.get("/user").json<User>();
182
+ const posts = await client.get("/posts").json<Post[]>();
179
183
 
180
- // Static usage - one-off requests
181
- const data = await Fetchy.json("https://api.example.com/data");
184
+ // POST with body
185
+ const result = await client.post("/create", {
186
+ body: { name: "John" }
187
+ }).json();
182
188
 
183
189
  // Safe mode
184
- const result = await Fetchy.sjson("https://api.example.com/data");
190
+ const data = await client.sget("/data").json<Data>();
191
+ if (data !== null) {
192
+ // Handle successful response
193
+ }
194
+ ```
195
+
196
+ ### `fy(options?)` Function
197
+
198
+ Shorthand for creating a new `Fetchy` instance.
199
+ ```ts
200
+ const client = fy({
201
+ base: "https://api.example.com",
202
+ bearer: "token"
203
+ });
204
+
205
+ const user = await client.get("/user").json<User>();
206
+ ```
207
+
208
+ Equivalent to:
209
+ ```ts
210
+ const client = new Fetchy({
211
+ base: "https://api.example.com",
212
+ bearer: "token"
213
+ });
185
214
  ```
186
215
 
187
216
  ## Configuration
188
217
 
189
218
  ### `FetchyOptions`
190
-
191
219
  ```ts
192
220
  interface FetchyOptions extends Omit<RequestInit, "body"> {
193
221
  // Request URL (allows null url parameter with this option)
194
- url?: string | URL;
222
+ url?: string | URL | Request;
223
+
224
+ // Base URL prepended to request URL (only for string/URL, not Request)
225
+ base?: string | URL;
195
226
 
196
- // Request body (auto-serializes JSON; ReadableStream is NOT supported)
197
- body?: JSONValue | FormData | URLSearchParams | Blob | ArrayBuffer | string;
227
+ // Request body (auto-serializes JSON objects)
228
+ body?: JSONValue | BodyInit;
198
229
 
199
- // Timeout in seconds (default: 15, set to 0 to disable)
230
+ // Timeout in seconds (default: 15)
200
231
  timeout?: number;
201
232
 
202
233
  // Retry configuration (set to false to disable)
203
- retry?: {
204
- interval?: number; // Base interval in seconds (default: 3)
205
- maxInterval?: number; // Maximum interval cap (default: 30)
206
- maxAttempts?: number; // Maximum retry attempts (default: 3)
207
- retryAfter?: boolean; // Respect Retry-After header (default: true)
208
- } | false;
234
+ retry?: RetryOptions | false;
209
235
 
210
236
  // Bearer token (automatically adds "Bearer " prefix)
211
237
  bearer?: string;
212
238
 
213
- // Initial jitter delay in seconds before request (default: 0)
214
- delay?: number;
239
+ // Maximum jitter delay in seconds before request (default: 0)
240
+ jitter?: number;
215
241
 
216
242
  // Use native fetch error behavior (no HTTPStatusError on 4xx/5xx)
217
- native?: true;
243
+ native?: boolean;
218
244
  }
245
+
246
+ interface RetryOptions {
247
+ maxAttempts?: number; // Maximum retry attempts (default: 3)
248
+ interval?: number; // Base interval in seconds (default: 3)
249
+ maxInterval?: number; // Maximum interval cap (default: 30)
250
+ retryOnTimeout?: boolean; // Retry on timeout (default: true)
251
+ idempotentOnly?: boolean; // Only retry idempotent methods (default: false)
252
+ statusCodes?: number[]; // Status codes to retry (default: [500, 502, 503, 504, 408, 429])
253
+ respectHeaders?: string[]; // Headers to respect for retry timing
254
+ } // (default: ["retry-after", "ratelimit-reset", "x-ratelimit-reset"])
219
255
  ```
220
256
 
221
257
  #### Default Values
222
-
223
258
  ```ts
224
259
  {
225
260
  timeout: 15, // 15 seconds
226
- delay: 0, // No jitter delay
261
+ jitter: 0, // No jitter delay
262
+ native: false, // Throws HTTPStatusError on non-OK status
227
263
  retry: {
228
264
  maxAttempts: 3, // 3 retry attempts
229
265
  interval: 3, // 3 seconds base interval
230
266
  maxInterval: 30, // 30 seconds maximum interval
231
- retryAfter: true // Respect Retry-After header
232
- },
233
- native: undefined // Throws HTTPStatusError on non-OK status (4xx, 5xx)
267
+ retryOnTimeout: true, // Retry on timeout
268
+ idempotentOnly: false, // Retry all methods
269
+ statusCodes: [500, 502, 503, 504, 408, 429],
270
+ respectHeaders: ["retry-after", "ratelimit-reset", "x-ratelimit-reset"]
271
+ }
234
272
  }
235
273
  ```
236
274
 
@@ -249,43 +287,27 @@ The following headers are automatically set if not specified:
249
287
  - **Accept**: `application/json, text/plain`
250
288
  - **Content-Type**: Automatically determined based on body type:
251
289
  - `string`, `URLSearchParams`, `FormData`, `Blob` with type: Not set (native fetch handles it)
252
- - `JSONValue`: `application/json`
290
+ - `JSONValue` (objects, arrays, numbers, booleans): `application/json`
253
291
  - `Blob` without type, `ArrayBuffer`: `application/octet-stream`
254
292
  - **Authorization**: `Bearer ${options.bearer}` if bearer is provided
255
293
 
256
- **Note:** If you pass a body through a Request object, Content-Type is NOT set automatically by this package.
294
+ **Note:** Headers from Request objects are preserved and merged with option headers.
257
295
 
258
296
  ## Error Handling
259
297
 
260
298
  ### HTTPStatusError
261
299
 
262
300
  Thrown when response status is not OK (4xx, 5xx) unless `native: true` is set.
263
-
264
301
  ```ts
302
+ import { fetchy, HTTPStatusError } from "@scirexs/fetchy";
303
+
265
304
  try {
266
305
  await fetchy("https://api.example.com/data");
267
306
  } catch (error) {
268
307
  if (error instanceof HTTPStatusError) {
269
- console.error(error.status); // 404
270
- console.error(error.body); // Response body text
271
- console.error(error.message); // "404 Not Found: (no response body)"
272
- }
273
- }
274
- ```
275
-
276
- ### RedirectError
277
-
278
- Thrown when `redirect: "error"` is set and a redirect response (3xx) is received.
279
-
280
- ```ts
281
- try {
282
- await fetchy("https://example.com/redirect", {
283
- redirect: "error"
284
- });
285
- } catch (error) {
286
- if (error instanceof RedirectError) {
287
- console.error(error.status); // 301
288
- console.error(error.message); // "301 Moved Permanently"
308
+ console.error(error.status); // 404
309
+ console.error(error.response); // Response object
310
+ console.error(error.message); // "404 https://api.example.com/data"
289
311
  }
290
312
  }
291
313
  ```
@@ -298,71 +320,74 @@ Other errors (network failures, timeout, abort) are thrown as standard errors:
298
320
 
299
321
  ### Safe Error Handling
300
322
 
301
- Use `sfetchy()` or `Fetchy.safe()` to return `null` instead of throwing:
302
-
323
+ Use `sfetchy()` or safe methods to return `null` instead of throwing:
303
324
  ```ts
304
- const data = await sfetchy("https://api.example.com/data", {}, "json");
305
- if (data === null) {
325
+ // Safe fetch - returns null on any error
326
+ const response = await sfetchy("https://api.example.com/data");
327
+ if (response === null) {
306
328
  // Handle error gracefully
307
329
  }
330
+
331
+ // Safe parsing methods - return null on error
332
+ const data = await fetchy("https://api.example.com/data").json<Data>();
333
+ if (data !== null) {
334
+ // Process data
335
+ }
308
336
  ```
309
337
 
310
338
  ### Native Mode
311
339
 
312
340
  Set `native: true` to disable HTTPStatusError and get native fetch behavior:
313
-
314
341
  ```ts
315
342
  const response = await fetchy("https://api.example.com/data", {
316
343
  native: true
317
344
  });
318
345
  // Returns Response even for 4xx/5xx status codes
346
+ if (!response.ok) {
347
+ console.error("Request failed");
348
+ }
319
349
  ```
320
350
 
321
351
  ## Usage Examples
322
352
 
323
353
  ### Basic Requests
324
-
325
354
  ```ts
326
355
  import { fetchy, sfetchy } from "@scirexs/fetchy";
327
356
 
328
357
  // GET with automatic JSON parsing
329
- const data = await fetchy<User[]>("https://api.example.com/users", {}, "json");
358
+ const users = await fetchy("https://api.example.com/users").json<User[]>();
330
359
 
331
360
  // POST with JSON body
332
361
  const result = await fetchy("https://api.example.com/create", {
362
+ method: "POST",
333
363
  body: { name: "John", email: "john@example.com" }
334
- }, "json");
364
+ }).json();
335
365
 
336
366
  // Custom headers
337
367
  const response = await fetchy("https://api.example.com/data", {
338
368
  headers: { "X-Custom-Header": "value" }
339
369
  });
340
370
 
341
- // Reuse options as preset configuration
342
- const options: FetchyOptions = {
343
- url: "https://api.example.com/data",
344
- retry: false
345
- };
346
- await fetchy(null, options);
347
- await fetchy(null, options);
371
+ // Using base URL
372
+ const data = await fetchy("/users", {
373
+ base: "https://api.example.com"
374
+ }).json();
348
375
  ```
349
376
 
350
377
  ### Authentication
351
-
352
378
  ```ts
353
379
  // Bearer token authentication
354
- const user = await fetchy<User>("https://api.example.com/me", {
380
+ const user = await fetchy("https://api.example.com/me", {
355
381
  bearer: "your-access-token"
356
- }, "json");
382
+ }).json<User>();
357
383
 
358
384
  // Custom authorization
359
385
  const data = await fetchy("https://api.example.com/data", {
360
386
  headers: { "Authorization": "Basic " + btoa("user:pass") }
361
- }, "json");
387
+ }).json();
362
388
  ```
363
389
 
364
390
  ### Timeout and Retry
365
-
366
391
  ```ts
367
392
  // Custom timeout
368
393
  const response = await fetchy("https://slow-api.example.com", {
@@ -370,15 +395,20 @@ const response = await fetchy("https://slow-api.example.com", {
370
395
  });
371
396
 
372
397
  // Retry with exponential backoff
373
- // Intervals: 1s (3^0), 3s (3^1), 9s (3^2), 27s (3^3), capped at maxInterval
398
+ // Intervals: 3s, 6s, 12s, 24s (capped at maxInterval)
374
399
  const data = await fetchy("https://api.example.com/data", {
375
400
  retry: {
376
401
  maxAttempts: 5,
377
402
  interval: 3,
378
- maxInterval: 60,
379
- retryAfter: true
403
+ maxInterval: 60
380
404
  }
381
- }, "json");
405
+ }).json();
406
+
407
+ // Retry only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE)
408
+ const result = await fetchy("https://api.example.com/update", {
409
+ method: "POST",
410
+ retry: { idempotentOnly: true } // Won't retry POST
411
+ }).json();
382
412
 
383
413
  // Disable retry
384
414
  const response = await fetchy("https://api.example.com/data", {
@@ -387,21 +417,20 @@ const response = await fetchy("https://api.example.com/data", {
387
417
  ```
388
418
 
389
419
  ### Error Handling Patterns
390
-
391
420
  ```ts
392
- import { fetchy, sfetchy, HTTPStatusError, RedirectError } from "@scirexs/fetchy";
421
+ import { fetchy, sfetchy, HTTPStatusError } from "@scirexs/fetchy";
393
422
 
394
423
  // Default: throws on error
395
424
  try {
396
- const data = await fetchy("https://api.example.com/data", {}, "json");
425
+ const data = await fetchy("https://api.example.com/data").json();
397
426
  } catch (error) {
398
427
  if (error instanceof HTTPStatusError) {
399
- console.error(`HTTP ${error.status}: ${error.body}`);
428
+ console.error(`HTTP ${error.status}:`, error.response);
400
429
  }
401
430
  }
402
431
 
403
432
  // Safe mode: returns null
404
- const data = await sfetchy("https://api.example.com/data", {}, "json");
433
+ const data = await sfetchy("https://api.example.com/data").json();
405
434
  if (data === null) {
406
435
  console.log("Request failed, using default");
407
436
  }
@@ -415,98 +444,131 @@ if (!response.ok) {
415
444
  }
416
445
  ```
417
446
 
418
- ### Fluent API
419
-
447
+ ### Fluent API with HTTP Methods
420
448
  ```ts
449
+ import { Fetchy, fy } from "@scirexs/fetchy";
450
+
421
451
  // Create reusable client
422
- const api = new Fetchy({
423
- url: "https://api.example.com",
452
+ const api = fy({
453
+ base: "https://api.example.com",
424
454
  bearer: "token",
425
455
  timeout: 10,
426
456
  retry: { maxAttempts: 3 }
427
457
  });
428
458
 
429
- // Instance methods
430
- const users = await api.json<User[]>("/users");
431
- const post = await api.json<Post>("/posts/1");
432
- const text = await api.text("/readme.txt");
459
+ // HTTP method shortcuts
460
+ const users = await api.get("/users").json<User[]>();
461
+ const post = await api.get("/posts/1").json<Post>();
462
+ const created = await api.post("/posts", {
463
+ body: { title: "New Post" }
464
+ }).json();
465
+ const updated = await api.put("/posts/1", {
466
+ body: { title: "Updated" }
467
+ }).json();
468
+ const patched = await api.patch("/posts/1", {
469
+ body: { views: 100 }
470
+ }).json();
471
+ await api.delete("/posts/1");
433
472
 
434
473
  // Safe methods
435
- const data = await api.sjson("/maybe-fails");
474
+ const data = await api.sget("/maybe-fails").json();
436
475
  if (data !== null) {
437
476
  // Process data
438
477
  }
439
478
 
440
- // Static methods for one-off requests
441
- const response = await Fetchy.fetch("https://example.com");
442
- const json = await Fetchy.json("https://api.example.com/data");
479
+ // Override instance options per request
480
+ const text = await api.get("/readme.txt", {
481
+ timeout: 5
482
+ }).text();
443
483
  ```
444
484
 
445
485
  ### Advanced Usage
446
486
 
447
- #### Jitter and Delays
487
+ #### Jitter for Load Distribution
448
488
  ```ts
449
- // Jitter to prevent thundering herd
489
+ // Add randomized delay to prevent thundering herd
450
490
  const response = await fetchy("https://api.example.com/data", {
451
- delay: 2, // Random delay up to 2 seconds
491
+ jitter: 2, // Random delay up to 2 seconds before each request
452
492
  retry: { maxAttempts: 3 }
453
493
  });
454
494
  ```
455
495
 
456
496
  #### Abort Signals
457
497
  ```ts
458
- // Combined abort signals
498
+ // Manual abort control
459
499
  const controller = new AbortController();
460
- const request = new Request("https://api.example.com/data", {
500
+ const promise = fetchy("https://api.example.com/data", {
461
501
  signal: controller.signal
462
502
  });
463
503
 
464
504
  setTimeout(() => controller.abort(), 5000);
465
505
 
466
- const response = await fetchy(request, {
467
- signal: AbortSignal.timeout(10000)
506
+ try {
507
+ await promise;
508
+ } catch (error) {
509
+ // Aborted after 5 seconds
510
+ }
511
+
512
+ // Combining timeout with manual abort
513
+ const controller = new AbortController();
514
+ await fetchy("https://api.example.com/data", {
515
+ timeout: 10,
516
+ signal: controller.signal
468
517
  });
518
+ // Request will abort after 10 seconds OR when controller.abort() is called
469
519
  ```
470
520
 
471
- #### Form Data
521
+ #### Form Data and File Uploads
472
522
  ```ts
473
523
  // Form data upload
474
524
  const formData = new FormData();
475
- formData.append("file", blob);
525
+ formData.append("file", blob, "filename.png");
476
526
  formData.append("name", "example");
477
527
 
478
528
  await fetchy("https://api.example.com/upload", {
529
+ method: "POST",
479
530
  body: formData
480
531
  });
481
532
 
482
533
  // URL-encoded form
483
- const params = new URLSearchParams({ key: "value" });
534
+ const params = new URLSearchParams({ key: "value", foo: "bar" });
484
535
  await fetchy("https://api.example.com/form", {
536
+ method: "POST",
485
537
  body: params
486
538
  });
487
539
  ```
488
540
 
489
- #### Testing Utilities
490
-
491
- When writing tests for code that uses `fetchy`, you may need to simulate immediate failures without triggering retry logic. Use the `NO_RETRY_ERROR` constant to bypass all retry attempts:
541
+ #### Streaming with ReadableStream
492
542
  ```ts
493
- import { fetchy, NO_RETRY_ERROR } from "@scirexs/fetchy";
543
+ // ReadableStream can be used via Request object
544
+ const stream = new ReadableStream({
545
+ start(controller) {
546
+ controller.enqueue(new TextEncoder().encode("chunk 1\n"));
547
+ controller.enqueue(new TextEncoder().encode("chunk 2\n"));
548
+ controller.close();
549
+ }
550
+ });
494
551
 
495
- const originalFetch = globalThis.fetch;
496
- globalThis.fetch = () => Promise.reject(new Error(NO_RETRY_ERROR));
552
+ const response = await fetchy("https://api.example.com/stream", {
553
+ method: "POST",
554
+ body: stream
555
+ });
556
+ ```
497
557
 
498
- try {
499
- await fetchy("https://api.example.com/data");
500
- } catch (error) {
501
- // Error is thrown immediately without retries
502
- console.error(error);
503
- } finally {
504
- globalThis.fetch = originalFetch;
505
- }
558
+ #### Retry-After Header Respect
559
+ ```ts
560
+ // Automatically respects Retry-After, RateLimit-Reset, X-RateLimit-Reset headers
561
+ const data = await fetchy("https://api.example.com/rate-limited", {
562
+ retry: {
563
+ maxAttempts: 5,
564
+ interval: 1, // Minimum interval if header not present
565
+ respectHeaders: ["retry-after", "ratelimit-reset"]
566
+ }
567
+ }).json();
568
+ // If response has "Retry-After: 10", will wait 10 seconds before retry
506
569
  ```
507
570
 
508
571
  ### Type-Safe API Responses
509
-
510
572
  ```ts
511
573
  interface ApiResponse<T> {
512
574
  success: boolean;
@@ -520,42 +582,88 @@ interface Todo {
520
582
  completed: boolean;
521
583
  }
522
584
 
523
- const response = await fetchy<ApiResponse<Todo>>(
524
- "https://api.example.com/todos/1",
525
- {},
526
- "json"
527
- );
585
+ const response = await fetchy("https://api.example.com/todos/1")
586
+ .json<ApiResponse<Todo>>();
528
587
 
529
588
  if (response.success) {
530
589
  console.log(response.data.title); // Fully typed
531
590
  }
532
- ```
533
591
 
534
- ## Limitations
592
+ // With safe parsing
593
+ const result = await sfetchy("https://api.example.com/todos/1")
594
+ .json<ApiResponse<Todo>>();
535
595
 
536
- ### Content-Type Header with Request Objects
596
+ if (result !== null && result.success) {
597
+ console.log(result.data.completed);
598
+ }
599
+ ```
537
600
 
538
- When a body is set in a Request object, the Content-Type header is NOT set automatically by this package. Use the `url` property in `FetchyOptions` instead to benefit from automatic header configuration:
601
+ ## Best Practices
539
602
 
603
+ ### 1. Use Base URL for API Clients
540
604
  ```ts
541
- // Instead of this:
542
- const request = new Request("https://api.example.com", { body: jsonData });
543
- await fetchy(request);
544
-
545
- // Do this:
546
- await fetchy(null, {
547
- url: "https://api.example.com",
548
- body: jsonData
605
+ const api = fy({
606
+ base: "https://api.example.com",
607
+ bearer: process.env.API_TOKEN,
608
+ timeout: 10
549
609
  });
610
+
611
+ // All requests are relative to base
612
+ await api.get("/users").json();
613
+ await api.post("/posts", { body: data }).json();
614
+ ```
615
+
616
+ ### 2. Choose Safe vs. Throwing Behavior
617
+ ```ts
618
+ // For critical operations: use regular methods (throws on error)
619
+ try {
620
+ const result = await fetchy(url).json();
621
+ // Process result
622
+ } catch (error) {
623
+ // Handle error explicitly
624
+ }
625
+
626
+ // For optional data: use safe methods (returns null)
627
+ const data = await fetchy(url).sjson();
628
+ if (data !== null) {
629
+ // Process data
630
+ }
631
+ // Continue regardless of result
550
632
  ```
551
633
 
552
- ### ReadableStream as Body
634
+ ### 3. Configure Retry for Resilience
635
+ ```ts
636
+ // Aggressive retry for critical operations
637
+ const result = await fetchy(url, {
638
+ retry: {
639
+ maxAttempts: 5,
640
+ interval: 2,
641
+ maxInterval: 30,
642
+ retryOnTimeout: true
643
+ }
644
+ }).json();
645
+
646
+ // No retry for operations that must be fast
647
+ const data = await fetchy(url, {
648
+ retry: false,
649
+ timeout: 2
650
+ }).json();
651
+ ```
553
652
 
554
- `FetchyOptions` does not accept ReadableStream as a body. If you need to use ReadableStream, create a Request object with the stream and pass it to `fetchy()`.
653
+ ### 4. Use Method Shortcuts for Clarity
654
+ ```ts
655
+ const api = fy({ base: "https://api.example.com" });
555
656
 
556
- ### Redirect Error Handling
657
+ // Clear and concise
658
+ await api.get("/users").json();
659
+ await api.post("/users", { body: newUser }).json();
660
+ await api.delete("/users/123");
557
661
 
558
- When `redirect` is set to `"error"`, this package throws a custom `RedirectError` (instead of native TypeError) to enable proper retry handling for redirect responses.
662
+ // Instead of
663
+ await api.fetch("/users", { method: "GET" }).json();
664
+ await api.fetch("/users", { method: "POST", body: newUser }).json();
665
+ await api.fetch("/users/123", { method: "DELETE" });
666
+ ```
559
667
 
560
668
  ## License
561
669