@scirexs/fetchy 0.6.1 → 0.8.1

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