@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 +314 -206
- package/esm/main.js +1 -1
- package/esm/mod.js +1 -1
- package/package.json +1 -1
- package/types/main.d.ts +206 -210
- package/types/main.d.ts.map +1 -1
- package/types/mod.d.ts +2 -2
- package/types/mod.d.ts.map +1 -1
- package/types/types.d.ts +121 -18
- package/types/types.d.ts.map +1 -1
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 ~
|
|
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
|
|
37
|
-
const
|
|
35
|
+
// Simple GET request with automatic JSON parsing
|
|
36
|
+
const user = await fetchy("https://api.example.com/user/1").json<User>();
|
|
38
37
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 =
|
|
45
|
+
const client = fy({
|
|
50
46
|
bearer: "token",
|
|
51
47
|
timeout: 10,
|
|
52
48
|
retry: { maxAttempts: 5 }
|
|
53
49
|
});
|
|
54
|
-
const
|
|
50
|
+
const posts = await client.get("/posts").json<Post[]>();
|
|
55
51
|
```
|
|
56
52
|
|
|
57
53
|
## API Reference
|
|
58
54
|
|
|
59
|
-
### `fetchy(url
|
|
55
|
+
### `fetchy(url?, options?)`
|
|
60
56
|
|
|
61
|
-
Performs an HTTP request with enhanced features.
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
//
|
|
85
|
-
const user = await fetchy
|
|
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
|
-
}
|
|
88
|
+
}).json();
|
|
92
89
|
|
|
93
90
|
// Binary data
|
|
94
|
-
const image = await fetchy("https://api.example.com/image.png"
|
|
91
|
+
const image = await fetchy("https://api.example.com/image.png").bytes();
|
|
95
92
|
```
|
|
96
93
|
|
|
97
|
-
### `sfetchy(url
|
|
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
|
-
|
|
104
|
+
`FetchySafeResponse` - A promise-like object that extends `Promise<Response | null>` with the same parsing methods as `FetchyResponse`.
|
|
108
105
|
|
|
109
|
-
|
|
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
|
|
114
|
-
if (
|
|
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
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
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
|
|
132
|
+
A fluent HTTP client class that provides instance methods.
|
|
128
133
|
|
|
129
|
-
####
|
|
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
|
-
//
|
|
135
|
-
await client.fetch(url?)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await client.
|
|
139
|
-
await client.
|
|
140
|
-
await client.
|
|
141
|
-
await client.
|
|
142
|
-
await client.
|
|
143
|
-
await client.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await client.
|
|
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
|
-
|
|
150
|
-
|
|
164
|
+
All methods can be chained with parsing methods:
|
|
151
165
|
```ts
|
|
152
|
-
|
|
153
|
-
await
|
|
154
|
-
await
|
|
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.
|
|
178
|
-
const posts = await client.json<Post[]>(
|
|
181
|
+
const user = await client.get("/user").json<User>();
|
|
182
|
+
const posts = await client.get("/posts").json<Post[]>();
|
|
179
183
|
|
|
180
|
-
//
|
|
181
|
-
const
|
|
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
|
|
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
|
|
197
|
-
body?: JSONValue |
|
|
227
|
+
// Request body (auto-serializes JSON objects)
|
|
228
|
+
body?: JSONValue | BodyInit;
|
|
198
229
|
|
|
199
|
-
// Timeout in seconds (default: 15
|
|
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
|
-
//
|
|
214
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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:**
|
|
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);
|
|
270
|
-
console.error(error.
|
|
271
|
-
console.error(error.message);
|
|
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
|
|
302
|
-
|
|
323
|
+
Use `sfetchy()` or safe methods to return `null` instead of throwing:
|
|
303
324
|
```ts
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
//
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
380
|
+
const user = await fetchy("https://api.example.com/me", {
|
|
355
381
|
bearer: "your-access-token"
|
|
356
|
-
}
|
|
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
|
-
}
|
|
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:
|
|
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
|
-
}
|
|
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
|
|
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"
|
|
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}
|
|
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"
|
|
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 =
|
|
423
|
-
|
|
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
|
-
//
|
|
430
|
-
const users = await api.json<User[]>(
|
|
431
|
-
const post = await api.
|
|
432
|
-
const
|
|
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.
|
|
474
|
+
const data = await api.sget("/maybe-fails").json();
|
|
436
475
|
if (data !== null) {
|
|
437
476
|
// Process data
|
|
438
477
|
}
|
|
439
478
|
|
|
440
|
-
//
|
|
441
|
-
const
|
|
442
|
-
|
|
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
|
|
487
|
+
#### Jitter for Load Distribution
|
|
448
488
|
```ts
|
|
449
|
-
//
|
|
489
|
+
// Add randomized delay to prevent thundering herd
|
|
450
490
|
const response = await fetchy("https://api.example.com/data", {
|
|
451
|
-
|
|
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
|
-
//
|
|
498
|
+
// Manual abort control
|
|
459
499
|
const controller = new AbortController();
|
|
460
|
-
const
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
|
496
|
-
|
|
552
|
+
const response = await fetchy("https://api.example.com/stream", {
|
|
553
|
+
method: "POST",
|
|
554
|
+
body: stream
|
|
555
|
+
});
|
|
556
|
+
```
|
|
497
557
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
524
|
-
|
|
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
|
-
|
|
592
|
+
// With safe parsing
|
|
593
|
+
const result = await sfetchy("https://api.example.com/todos/1")
|
|
594
|
+
.json<ApiResponse<Todo>>();
|
|
535
595
|
|
|
536
|
-
|
|
596
|
+
if (result !== null && result.success) {
|
|
597
|
+
console.log(result.data.completed);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
537
600
|
|
|
538
|
-
|
|
601
|
+
## Best Practices
|
|
539
602
|
|
|
603
|
+
### 1. Use Base URL for API Clients
|
|
540
604
|
```ts
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
653
|
+
### 4. Use Method Shortcuts for Clarity
|
|
654
|
+
```ts
|
|
655
|
+
const api = fy({ base: "https://api.example.com" });
|
|
555
656
|
|
|
556
|
-
|
|
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
|
-
|
|
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
|
|