@scirexs/fetchy 0.5.0 → 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
@@ -4,21 +4,22 @@
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.
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.
8
8
 
9
9
  ## Features
10
10
 
11
- - **Lightweight** - Zero dependencies, works in Deno, Node.js, and browsers
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
+ - **Fluent Interface** - Class-based API with both instance and static methods
20
+ - **HTTP Method Shortcuts** - Convenient methods for GET, POST, PUT, PATCH, DELETE
19
21
 
20
22
  ## Installation
21
-
22
23
  ```bash
23
24
  # npm
24
25
  npm install @scirexs/fetchy
@@ -28,151 +29,246 @@ deno add jsr:@scirexs/fetchy
28
29
  ```
29
30
 
30
31
  ## Quick Start
31
-
32
32
  ```ts
33
- import { fetchy, fetchyb } from "@scirexs/fetchy";
33
+ import { fetchy, sfetchy, Fetchy, fy } from "@scirexs/fetchy";
34
34
 
35
- // Simple GET request with timeout
36
- const response = await fetchy("https://api.example.com/data", {
37
- timeout: 10
38
- });
35
+ // Simple GET request with automatic JSON parsing
36
+ const user = await fetchy("https://api.example.com/user/1").json<User>();
39
37
 
40
- // Auto-parsed JSON response
41
- interface User {
42
- id: number;
43
- 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);
44
42
  }
45
43
 
46
- const user = await fetchyb<User>("https://api.example.com/user/1", "json");
47
- console.log(user?.name);
44
+ // Fluent API with reusable configuration
45
+ const client = fy({
46
+ bearer: "token",
47
+ timeout: 10,
48
+ retry: { maxAttempts: 5 }
49
+ });
50
+ const posts = await client.get("/posts").json<Post[]>();
48
51
  ```
49
52
 
50
53
  ## API Reference
51
54
 
52
- ### `fetchy(url, options?)`
55
+ ### `fetchy(url?, options?)`
53
56
 
54
- Performs an HTTP request and returns the raw Response object.
57
+ Performs an HTTP request with enhanced features. Returns a promise-like object that can be awaited directly or chained with parsing methods.
55
58
 
56
59
  #### Parameters
57
60
 
58
- - `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)
59
62
  - `options`: `FetchyOptions` (optional) - Configuration options
60
63
 
61
64
  #### Returns
62
65
 
63
- `Promise<Response>`; If `onError.onNative` is configured as `false`, returns `Promise<Response | null>`
66
+ `FetchyResponse` - A promise-like object that extends `Promise<Response>` with the following methods:
64
67
 
65
- #### 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
66
74
 
75
+ #### Example
67
76
  ```ts
68
- const response = await fetchy("https://api.example.com/data", {
77
+ // Get Response object
78
+ const response = await fetchy("https://api.example.com/data");
79
+
80
+ // Chain JSON parsing
81
+ const user = await fetchy("https://api.example.com/user").json<User>();
82
+
83
+ // POST with automatic body serialization
84
+ const result = await fetchy("https://api.example.com/create", {
69
85
  method: "POST",
70
- body: { key: "value" },
71
- timeout: 10,
72
- retry: { maxAttempts: 3, interval: 2 },
73
- bearer: "your-token-here"
74
- });
86
+ body: { name: "John", age: 30 },
87
+ bearer: "token"
88
+ }).json();
75
89
 
76
- if (response?.ok) {
77
- const data = await response.json();
78
- }
90
+ // Binary data
91
+ const image = await fetchy("https://api.example.com/image.png").bytes();
79
92
  ```
80
93
 
81
- ### `fetchyb(url, type?, options?)`
94
+ ### `sfetchy(url?, options?)`
82
95
 
83
- Performs an HTTP request and automatically parses the response body.
96
+ Performs an HTTP request with safe error handling. Returns `null` on any failure instead of throwing.
84
97
 
85
98
  #### Parameters
86
99
 
87
- - `url`: `string | URL | Request | null` - The request URL
88
- - `type`: `"text" | "json" | "bytes" | "auto"` (default: `"auto"`) - Response parsing type
89
- - `options`: `FetchyOptions` (optional) - Configuration options
100
+ Same as `fetchy()`.
90
101
 
91
102
  #### Returns
92
103
 
93
- `Promise<T | string | Uint8Array>`; If `onError.onNative` is configured as `false`, returns `Promise<T | string | Uint8Array | null>`
104
+ `FetchySafeResponse` - A promise-like object that extends `Promise<Response | null>` with the same parsing methods as `FetchyResponse`.
94
105
 
95
- #### 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
96
112
 
113
+ #### Example
97
114
  ```ts
98
- // Automatic type detection from Content-Type header
99
- const data = await fetchyb("https://api.example.com/data");
115
+ // Returns null instead of throwing on error
116
+ const response = await sfetchy("https://api.example.com/data");
117
+ if (response === null) {
118
+ console.log("Request failed gracefully");
119
+ } else {
120
+ const data = await response.json();
121
+ }
100
122
 
101
- // Explicit JSON parsing with type assertion
102
- interface Product {
103
- id: number;
104
- name: string;
105
- price: number;
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
106
127
  }
107
- const product = await fetchyb<Product>("https://api.example.com/product/1", "json");
128
+ ```
108
129
 
109
- // Text content
110
- const html = await fetchyb("https://example.com", "text");
130
+ ### `Fetchy` Class
111
131
 
112
- // Binary data
113
- const image = await fetchyb("https://example.com/image.png", "bytes");
132
+ A fluent HTTP client class that provides instance methods.
133
+
134
+ #### Constructor
135
+ ```ts
136
+ const client = new Fetchy(options?: FetchyOptions);
114
137
  ```
115
138
 
116
- ## Configuration
139
+ #### Instance Methods
140
+ ```ts
141
+ const client = new Fetchy(options);
142
+
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>
162
+ ```
163
+
164
+ All methods can be chained with parsing methods:
165
+ ```ts
166
+ await client.get("/users").json<User[]>();
167
+ await client.post("/create").json<Result>();
168
+ await client.safe("/data").text();
169
+ ```
117
170
 
118
- ### API Options
171
+ #### Example
172
+ ```ts
173
+ // Instance usage - reuse configuration
174
+ const client = new Fetchy({
175
+ base: "https://api.example.com",
176
+ bearer: "token123",
177
+ timeout: 10,
178
+ retry: { maxAttempts: 3 }
179
+ });
119
180
 
120
- #### `FetchyOptions`
181
+ const user = await client.get("/user").json<User>();
182
+ const posts = await client.get("/posts").json<Post[]>();
121
183
 
184
+ // POST with body
185
+ const result = await client.post("/create", {
186
+ body: { name: "John" }
187
+ }).json();
188
+
189
+ // Safe mode
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:
122
209
  ```ts
123
- interface FetchyOptions extends RequestInit {
124
- // Standard fetch options (method, headers, etc.)
125
- method?: string;
126
- headers?: HeadersInit;
210
+ const client = new Fetchy({
211
+ base: "https://api.example.com",
212
+ bearer: "token"
213
+ });
214
+ ```
215
+
216
+ ## Configuration
217
+
218
+ ### `FetchyOptions`
219
+ ```ts
220
+ interface FetchyOptions extends Omit<RequestInit, "body"> {
221
+ // Request URL (allows null url parameter with this option)
222
+ url?: string | URL | Request;
223
+
224
+ // Base URL prepended to request URL (only for string/URL, not Request)
225
+ base?: string | URL;
127
226
 
128
- // Request body (auto-serializes JSON; ReadableStream is NOT supported)
129
- // type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
130
- body?: JSONValue | FormData | URLSearchParams | Blob | ArrayBuffer | string;
227
+ // Request body (auto-serializes JSON objects)
228
+ body?: JSONValue | BodyInit;
131
229
 
132
230
  // Timeout in seconds (default: 15)
133
- timeout?: number; // Set to 0 to disable timeout
231
+ timeout?: number;
134
232
 
135
- // Retry configuration
136
- retry?: {
137
- maxAttempts?: number; // Maximum retry attempts (default: 3)
138
- interval?: number; // Base interval in seconds (default: 3)
139
- maxInterval?: number; // Maximum interval cap in seconds (default: 30)
140
- retryAfter?: boolean; // Respect Retry-After header (default: true)
141
- } | false; // Set to false to disable retry
233
+ // Retry configuration (set to false to disable)
234
+ retry?: RetryOptions | false;
142
235
 
143
236
  // Bearer token (automatically adds "Bearer " prefix)
144
237
  bearer?: string;
145
238
 
146
- // Error throwing behavior
147
- onError?: {
148
- onNative?: boolean; // Throw on native errors (default: true)
149
- onStatus?: boolean; // Throw on 4xx/5xx status (default: true)
150
- } | boolean; // Set to true to throw on all errors
239
+ // Maximum jitter delay in seconds before request (default: 0)
240
+ jitter?: number;
151
241
 
152
- // Initial jitter delay in seconds
153
- delay?: number;
154
-
155
- // URL for fetch (allows reusing FetchyOptions as preset configuration)
156
- url?: string | URL;
242
+ // Use native fetch error behavior (no HTTPStatusError on 4xx/5xx)
243
+ native?: boolean;
157
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"])
158
255
  ```
159
256
 
160
257
  #### Default Values
161
-
162
258
  ```ts
163
259
  {
164
260
  timeout: 15, // 15 seconds
165
- delay: 0, // No jitter delay
261
+ jitter: 0, // No jitter delay
262
+ native: false, // Throws HTTPStatusError on non-OK status
166
263
  retry: {
167
264
  maxAttempts: 3, // 3 retry attempts
168
265
  interval: 3, // 3 seconds base interval
169
266
  maxInterval: 30, // 30 seconds maximum interval
170
- retryAfter: true // Respect Retry-After header
171
- },
172
- onError: {
173
- onNative: true, // Throw native errors
174
- onStatus: true // Don't throw on HTTP errors
175
- },
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
+ }
176
272
  }
177
273
  ```
178
274
 
@@ -180,88 +276,118 @@ interface FetchyOptions extends RequestInit {
180
276
 
181
277
  #### Method
182
278
 
183
- If a body is provided without specifying a method, the method defaults to `"POST"`. When a Request object is passed as the `url` argument, its method is used.
279
+ - If body is provided without method: defaults to `"POST"`
280
+ - If Request object is passed: uses its method
281
+ - Otherwise: defaults to `"GET"`
184
282
 
185
283
  #### Headers
186
284
 
187
285
  The following headers are automatically set if not specified:
188
286
 
189
287
  - **Accept**: `application/json, text/plain`
190
- - **Content-Type**: Automatically determined based on the body type:
191
- - `string`, `URLSearchParams`, `FormData`, `Blob` with type: Not set by this package; [`fetch` will set it automatically](https://fetch.spec.whatwg.org/#concept-bodyinit-extract).
192
- - `JSONValue`: `application/json`
193
- - `Blob` without type: `application/octet-stream`
194
- - `ArrayBuffer`: `application/octet-stream`
195
- - **Authorization**: Set to `Bearer ${options.bearer}` if `options.bearer` is provided.
196
-
197
- **Note 1:** If you pass serialized JSON as the body (i.e., a string), Content-Type will be set to `text/plain;charset=UTF-8`. To ensure Content-Type is set to `application/json`, pass the JSON object directly instead of a serialized string.
288
+ - **Content-Type**: Automatically determined based on body type:
289
+ - `string`, `URLSearchParams`, `FormData`, `Blob` with type: Not set (native fetch handles it)
290
+ - `JSONValue` (objects, arrays, numbers, booleans): `application/json`
291
+ - `Blob` without type, `ArrayBuffer`: `application/octet-stream`
292
+ - **Authorization**: `Bearer ${options.bearer}` if bearer is provided
198
293
 
199
- **Note 2:** If you pass a body through a Request object, Content-Type will NOT be set automatically by this package.
294
+ **Note:** Headers from Request objects are preserved and merged with option headers.
200
295
 
201
296
  ## Error Handling
202
297
 
203
- ### Timeout
298
+ ### HTTPStatusError
204
299
 
205
- If the timeout duration specified in the `timeout` option is exceeded, the request is aborted using the standard `AbortSignal.timeout()` method. Note that there is no specific error class for timeout errors; they will be thrown as standard `AbortError`s.
300
+ Thrown when response status is not OK (4xx, 5xx) unless `native: true` is set.
301
+ ```ts
302
+ import { fetchy, HTTPStatusError } from "@scirexs/fetchy";
206
303
 
207
- ### HTTPStatusError
304
+ try {
305
+ await fetchy("https://api.example.com/data");
306
+ } catch (error) {
307
+ if (error instanceof HTTPStatusError) {
308
+ console.error(error.status); // 404
309
+ console.error(error.response); // Response object
310
+ console.error(error.message); // "404 https://api.example.com/data"
311
+ }
312
+ }
313
+ ```
208
314
 
209
- If `onStatus` is set to `true` (default), an `HTTPStatusError` will be thrown when the response status is outside the 2xx range. You can access the status and body through this error object. The error message format is: `404 Not Found: (no response body)`.
315
+ ### Native Errors
210
316
 
211
- ### RedirectError
317
+ Other errors (network failures, timeout, abort) are thrown as standard errors:
318
+ - `TypeError`: Network error, DNS resolution failure
319
+ - `DOMException`: Timeout or abort via AbortSignal
212
320
 
213
- If `redirect` is set to `"error"`, a `RedirectError` will be thrown when the response status is in the 3xx range. You can access the status through this error object. The error message format is: `301 Moved Permanently`.
321
+ ### Safe Error Handling
214
322
 
215
- ### Other Errors
323
+ Use `sfetchy()` or safe methods to return `null` instead of throwing:
324
+ ```ts
325
+ // Safe fetch - returns null on any error
326
+ const response = await sfetchy("https://api.example.com/data");
327
+ if (response === null) {
328
+ // Handle error gracefully
329
+ }
216
330
 
217
- If the `onNative` option is set to `true`, any other errors that occur will be thrown directly.
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
+ }
336
+ ```
337
+
338
+ ### Native Mode
339
+
340
+ Set `native: true` to disable HTTPStatusError and get native fetch behavior:
341
+ ```ts
342
+ const response = await fetchy("https://api.example.com/data", {
343
+ native: true
344
+ });
345
+ // Returns Response even for 4xx/5xx status codes
346
+ if (!response.ok) {
347
+ console.error("Request failed");
348
+ }
349
+ ```
218
350
 
219
351
  ## Usage Examples
220
352
 
221
353
  ### Basic Requests
222
-
223
354
  ```ts
224
- import { fetchy, fetchyb } from "@scirexs/fetchy";
355
+ import { fetchy, sfetchy } from "@scirexs/fetchy";
225
356
 
226
- // GET request
227
- const data = await fetchyb("https://api.example.com/data", "json");
357
+ // GET with automatic JSON parsing
358
+ const users = await fetchy("https://api.example.com/users").json<User[]>();
228
359
 
229
360
  // POST with JSON body
230
- const result = await fetchyb("https://api.example.com/create", "json", {
361
+ const result = await fetchy("https://api.example.com/create", {
362
+ method: "POST",
231
363
  body: { name: "John", email: "john@example.com" }
232
- });
364
+ }).json();
233
365
 
234
366
  // Custom headers
235
367
  const response = await fetchy("https://api.example.com/data", {
236
- headers: {
237
- "X-Custom-Header": "value"
238
- }
368
+ headers: { "X-Custom-Header": "value" }
239
369
  });
240
370
 
241
- // Reuse options as preset configuration (avoids Request object limitations)
242
- const options = { url: "https://api.example.com/data", retry: false };
243
- await fetchy(null, options);
244
- await fetchy(null, options);
371
+ // Using base URL
372
+ const data = await fetchy("/users", {
373
+ base: "https://api.example.com"
374
+ }).json();
245
375
  ```
246
376
 
247
377
  ### Authentication
248
-
249
378
  ```ts
250
379
  // Bearer token authentication
251
- const user = await fetchyb<User>("https://api.example.com/me", "json", {
380
+ const user = await fetchy("https://api.example.com/me", {
252
381
  bearer: "your-access-token"
253
- });
382
+ }).json<User>();
254
383
 
255
384
  // Custom authorization
256
- const data = await fetchyb("https://api.example.com/data", "json", {
257
- headers: {
258
- "Authorization": "Basic " + btoa("user:pass")
259
- }
260
- });
385
+ const data = await fetchy("https://api.example.com/data", {
386
+ headers: { "Authorization": "Basic " + btoa("user:pass") }
387
+ }).json();
261
388
  ```
262
389
 
263
390
  ### Timeout and Retry
264
-
265
391
  ```ts
266
392
  // Custom timeout
267
393
  const response = await fetchy("https://slow-api.example.com", {
@@ -269,15 +395,20 @@ const response = await fetchy("https://slow-api.example.com", {
269
395
  });
270
396
 
271
397
  // Retry with exponential backoff
272
- // Retry intervals: 1s, 3s (3^1), 9s (3^2), 27s (3^3), 60s (capped at maxInterval)
273
- const data = await fetchyb("https://api.example.com/data", "json", {
398
+ // Intervals: 3s, 6s, 12s, 24s (capped at maxInterval)
399
+ const data = await fetchy("https://api.example.com/data", {
274
400
  retry: {
275
- maxAttempts: 5, // Retry up to 5 times
276
- interval: 3, // Base interval for exponential backoff (interval^n)
277
- maxInterval: 60, // Cap at 60 seconds
278
- retryAfter: true // Respect Retry-After header
401
+ maxAttempts: 5,
402
+ interval: 3,
403
+ maxInterval: 60
279
404
  }
280
- });
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();
281
412
 
282
413
  // Disable retry
283
414
  const response = await fetchy("https://api.example.com/data", {
@@ -285,94 +416,159 @@ const response = await fetchy("https://api.example.com/data", {
285
416
  });
286
417
  ```
287
418
 
288
- ### Error Handling
289
-
419
+ ### Error Handling Patterns
290
420
  ```ts
291
- import { fetchy, HTTPStatusError, RedirectError } from "@scirexs/fetchy";
421
+ import { fetchy, sfetchy, HTTPStatusError } from "@scirexs/fetchy";
292
422
 
293
- // Throw on error and not ok response (default behavior)
423
+ // Default: throws on error
294
424
  try {
295
- const response = await fetchy("https://api.example.com/data");
425
+ const data = await fetchy("https://api.example.com/data").json();
296
426
  } catch (error) {
297
427
  if (error instanceof HTTPStatusError) {
298
- console.error("HTTP error:", error.message); // e.g., "404 Not Found: (no response body)"
299
- console.error("Status:", error.status);
300
- console.error("Body:", error.body);
428
+ console.error(`HTTP ${error.status}:`, error.response);
301
429
  }
302
430
  }
303
431
 
304
- // Return null on error
432
+ // Safe mode: returns null
433
+ const data = await sfetchy("https://api.example.com/data").json();
434
+ if (data === null) {
435
+ console.log("Request failed, using default");
436
+ }
437
+
438
+ // Native mode: no HTTPStatusError
305
439
  const response = await fetchy("https://api.example.com/data", {
306
- onError: false,
440
+ native: true
307
441
  });
308
- if (response === null) {
309
- console.log("Request failed");
442
+ if (!response.ok) {
443
+ console.error("Request failed with status", response.status);
310
444
  }
445
+ ```
311
446
 
312
- // Throw only on native errors
313
- try {
314
- const response = await fetchy("https://api.example.com/data", {
315
- onError: { onNative: true, onStatus: false } as const
316
- });
317
- } catch (error) {
318
- console.error("Request failed:", error);
319
- }
447
+ ### Fluent API with HTTP Methods
448
+ ```ts
449
+ import { Fetchy, fy } from "@scirexs/fetchy";
320
450
 
321
- // Handle redirects
322
- try {
323
- const response = await fetchy("https://example.com/redirect", {
324
- redirect: "error"
325
- });
326
- } catch (error) {
327
- if (error instanceof RedirectError) {
328
- console.error("Unexpected redirect:", error.message);
329
- console.error("Status:", error.status);
330
- }
451
+ // Create reusable client
452
+ const api = fy({
453
+ base: "https://api.example.com",
454
+ bearer: "token",
455
+ timeout: 10,
456
+ retry: { maxAttempts: 3 }
457
+ });
458
+
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");
472
+
473
+ // Safe methods
474
+ const data = await api.sget("/maybe-fails").json();
475
+ if (data !== null) {
476
+ // Process data
331
477
  }
478
+
479
+ // Override instance options per request
480
+ const text = await api.get("/readme.txt", {
481
+ timeout: 5
482
+ }).text();
332
483
  ```
333
484
 
334
485
  ### Advanced Usage
335
486
 
487
+ #### Jitter for Load Distribution
336
488
  ```ts
337
- // Jitter to prevent thundering herd
489
+ // Add randomized delay to prevent thundering herd
338
490
  const response = await fetchy("https://api.example.com/data", {
339
- delay: 2, // Random delay up to 2 seconds before request
491
+ jitter: 2, // Random delay up to 2 seconds before each request
340
492
  retry: { maxAttempts: 3 }
341
493
  });
494
+ ```
342
495
 
343
- // Combined abort signals
344
- const controller1 = new AbortController();
345
- const controller2 = new AbortController();
346
- const request = new Request("https://api.example.com/data", {
347
- signal: controller1.signal
496
+ #### Abort Signals
497
+ ```ts
498
+ // Manual abort control
499
+ const controller = new AbortController();
500
+ const promise = fetchy("https://api.example.com/data", {
501
+ signal: controller.signal
348
502
  });
349
503
 
350
- setTimeout(() => controller1.abort(), 5000); // Abort after 5 seconds
504
+ setTimeout(() => controller.abort(), 5000);
505
+
506
+ try {
507
+ await promise;
508
+ } catch (error) {
509
+ // Aborted after 5 seconds
510
+ }
351
511
 
352
- const response = await fetchy(request, {
353
- signal: controller2.signal
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
354
517
  });
518
+ // Request will abort after 10 seconds OR when controller.abort() is called
519
+ ```
355
520
 
521
+ #### Form Data and File Uploads
522
+ ```ts
356
523
  // Form data upload
357
524
  const formData = new FormData();
358
- formData.append("file", blob);
525
+ formData.append("file", blob, "filename.png");
359
526
  formData.append("name", "example");
360
527
 
361
- const response = await fetchy("https://api.example.com/upload", {
528
+ await fetchy("https://api.example.com/upload", {
529
+ method: "POST",
362
530
  body: formData
363
531
  });
364
532
 
365
533
  // URL-encoded form
366
- const params = new URLSearchParams();
367
- params.append("key", "value");
368
-
369
- const response = await fetchy("https://api.example.com/form", {
534
+ const params = new URLSearchParams({ key: "value", foo: "bar" });
535
+ await fetchy("https://api.example.com/form", {
536
+ method: "POST",
370
537
  body: params
371
538
  });
372
539
  ```
373
540
 
374
- ### Type-Safe API Responses
541
+ #### Streaming with ReadableStream
542
+ ```ts
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
+ });
375
551
 
552
+ const response = await fetchy("https://api.example.com/stream", {
553
+ method: "POST",
554
+ body: stream
555
+ });
556
+ ```
557
+
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
569
+ ```
570
+
571
+ ### Type-Safe API Responses
376
572
  ```ts
377
573
  interface ApiResponse<T> {
378
574
  success: boolean;
@@ -386,46 +582,88 @@ interface Todo {
386
582
  completed: boolean;
387
583
  }
388
584
 
389
- const response = await fetchyb<ApiResponse<Todo>>(
390
- "https://api.example.com/todos/1",
391
- "json"
392
- );
585
+ const response = await fetchy("https://api.example.com/todos/1")
586
+ .json<ApiResponse<Todo>>();
393
587
 
394
588
  if (response.success) {
395
589
  console.log(response.data.title); // Fully typed
396
590
  }
397
- ```
398
591
 
399
- ## Limitations
592
+ // With safe parsing
593
+ const result = await sfetchy("https://api.example.com/todos/1")
594
+ .json<ApiResponse<Todo>>();
400
595
 
401
- ### Return Type Inference
596
+ if (result !== null && result.success) {
597
+ console.log(result.data.completed);
598
+ }
599
+ ```
402
600
 
403
- When setting the `onError` property in `FetchyOptions`, the return type will include `null` even if you set it to `true` or `{ onNative: true }`. To prevent this and ensure a non-nullable return type, add `as const` to the `onError` property value:
601
+ ## Best Practices
404
602
 
603
+ ### 1. Use Base URL for API Clients
405
604
  ```ts
406
- interface User {
407
- id: number;
408
- name: string;
409
- }
605
+ const api = fy({
606
+ base: "https://api.example.com",
607
+ bearer: process.env.API_TOKEN,
608
+ timeout: 10
609
+ });
410
610
 
411
- const options = { timeout: 5, onError: { onNative: true, onStatus: false } as const }; // Add `as const`
412
- const response = await fetchy("https://api.example.com/todos/1", "json", options);
413
- // `response` is User (not User | null)
611
+ // All requests are relative to base
612
+ await api.get("/users").json();
613
+ await api.post("/posts", { body: data }).json();
414
614
  ```
415
615
 
416
- ### Content-Type Header with Request Objects
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
+ }
417
625
 
418
- When a body is set in a Request object, the Content-Type header is NOT set automatically by this package. Therefore, when using Request objects, you must explicitly set the Content-Type header for any body types other than those automatically handled by the native `fetch` API.
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
632
+ ```
419
633
 
420
- This limitation can be avoided by using the `url` property in `FetchyOptions` instead of Request objects. This approach allows you to benefit from all automatic header configuration features while still maintaining reusable preset configurations. See the "Reuse options as preset configuration" example in the [Basic Requests](#basic-requests) section.
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();
421
645
 
422
- ### ReadableStream as Body
646
+ // No retry for operations that must be fast
647
+ const data = await fetchy(url, {
648
+ retry: false,
649
+ timeout: 2
650
+ }).json();
651
+ ```
423
652
 
424
- `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" });
425
656
 
426
- ### 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");
427
661
 
428
- When `redirect` is set to `"error"`, this package throws a custom `RedirectError` (instead of the 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
+ ```
429
667
 
430
668
  ## License
431
669