@fgrzl/fetch 1.1.0-alpha.1 → 1.1.0-alpha.3
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 +21 -1083
- package/dist/client.d.ts +34 -12
- package/dist/client.js +40 -14
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,7 +24,12 @@ npm install @fgrzl/fetch
|
|
|
24
24
|
```ts
|
|
25
25
|
import api from "@fgrzl/fetch";
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const response = await api.get("/api/user");
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
console.log(response.data); // Your typed data
|
|
30
|
+
} else {
|
|
31
|
+
console.error(`Error ${response.status}:`, response.error?.message);
|
|
32
|
+
}
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
Or create a custom instance:
|
|
@@ -44,1095 +49,28 @@ useCSRF(client, {
|
|
|
44
49
|
useUnauthorized(client, {
|
|
45
50
|
loginPath: "/login",
|
|
46
51
|
});
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## 🧩 Middleware
|
|
50
|
-
|
|
51
|
-
### Request Middleware
|
|
52
|
-
|
|
53
|
-
Request middleware functions run before the HTTP request is sent, allowing you to modify request options and URLs.
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { FetchClient, RequestMiddleware } from "@fgrzl/fetch";
|
|
57
|
-
|
|
58
|
-
const client = new FetchClient();
|
|
59
|
-
|
|
60
|
-
// Add authentication header
|
|
61
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
62
|
-
const token = localStorage.getItem("auth-token");
|
|
63
|
-
const headers = {
|
|
64
|
-
...req.headers,
|
|
65
|
-
...(token && { Authorization: `Bearer ${token}` }),
|
|
66
|
-
};
|
|
67
|
-
return [{ ...req, headers }, url];
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Add debug information
|
|
71
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
72
|
-
const headers = {
|
|
73
|
-
...req.headers,
|
|
74
|
-
"X-Debug": "true",
|
|
75
|
-
"X-Timestamp": new Date().toISOString(),
|
|
76
|
-
};
|
|
77
|
-
return [{ ...req, headers }, url];
|
|
78
|
-
});
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Response Middleware
|
|
82
|
-
|
|
83
|
-
Response middleware functions run after the HTTP response is received, allowing you to process or modify responses.
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { ResponseMiddleware } from "@fgrzl/fetch";
|
|
87
|
-
|
|
88
|
-
// Log response times
|
|
89
|
-
client.useResponseMiddleware(async (response) => {
|
|
90
|
-
console.log(`Request to ${response.url} took ${performance.now()}ms`);
|
|
91
|
-
return response;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Extract and store updated auth tokens
|
|
95
|
-
client.useResponseMiddleware(async (response) => {
|
|
96
|
-
const newToken = response.headers.get("X-New-Auth-Token");
|
|
97
|
-
if (newToken) {
|
|
98
|
-
localStorage.setItem("auth-token", newToken);
|
|
99
|
-
}
|
|
100
|
-
return response;
|
|
101
|
-
});
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Middleware Execution Order
|
|
105
|
-
|
|
106
|
-
Middlewares execute in the order they are registered:
|
|
107
|
-
|
|
108
|
-
1. **Request middlewares**: Execute in registration order before the request
|
|
109
|
-
2. **Response middlewares**: Execute in registration order after the response
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
const client = new FetchClient();
|
|
113
|
-
|
|
114
|
-
// These will execute in this exact order:
|
|
115
|
-
client.useRequestMiddleware(first); // 1st: runs first
|
|
116
|
-
client.useRequestMiddleware(second); // 2nd: runs second
|
|
117
|
-
client.useRequestMiddleware(third); // 3rd: runs third
|
|
118
|
-
|
|
119
|
-
client.useResponseMiddleware(alpha); // 1st: processes response first
|
|
120
|
-
client.useResponseMiddleware(beta); // 2nd: processes response second
|
|
121
|
-
client.useResponseMiddleware(gamma); // 3rd: processes response third
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## 🔄 Common Patterns
|
|
125
|
-
|
|
126
|
-
### Authentication with Token Retry
|
|
127
|
-
|
|
128
|
-
Automatically retry requests with fresh tokens when authentication fails:
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
import { FetchClient, HttpError } from "@fgrzl/fetch";
|
|
132
|
-
|
|
133
|
-
const client = new FetchClient();
|
|
134
|
-
|
|
135
|
-
// Request middleware: Add auth token
|
|
136
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
137
|
-
const token = localStorage.getItem("auth-token");
|
|
138
|
-
const headers = {
|
|
139
|
-
...req.headers,
|
|
140
|
-
...(token && { Authorization: `Bearer ${token}` }),
|
|
141
|
-
};
|
|
142
|
-
return [{ ...req, headers }, url];
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Response middleware: Handle token refresh
|
|
146
|
-
client.useResponseMiddleware(async (response) => {
|
|
147
|
-
if (response.status === 401) {
|
|
148
|
-
// Try to refresh the token
|
|
149
|
-
const refreshToken = localStorage.getItem("refresh-token");
|
|
150
|
-
if (refreshToken) {
|
|
151
|
-
try {
|
|
152
|
-
const refreshResponse = await fetch("/auth/refresh", {
|
|
153
|
-
method: "POST",
|
|
154
|
-
headers: { Authorization: `Bearer ${refreshToken}` },
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
if (refreshResponse.ok) {
|
|
158
|
-
const { access_token } = await refreshResponse.json();
|
|
159
|
-
localStorage.setItem("auth-token", access_token);
|
|
160
|
-
|
|
161
|
-
// Clone and retry the original request
|
|
162
|
-
const retryResponse = await fetch(response.url, {
|
|
163
|
-
...response,
|
|
164
|
-
headers: {
|
|
165
|
-
...response.headers,
|
|
166
|
-
Authorization: `Bearer ${access_token}`,
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
return retryResponse;
|
|
170
|
-
}
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error("Token refresh failed:", error);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Redirect to login if refresh fails
|
|
177
|
-
window.location.href = "/login";
|
|
178
|
-
}
|
|
179
|
-
return response;
|
|
180
|
-
});
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Request Correlation IDs
|
|
184
|
-
|
|
185
|
-
Track requests across services with correlation IDs:
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { v4 as uuidv4 } from "uuid";
|
|
189
|
-
|
|
190
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
191
|
-
const correlationId = uuidv4();
|
|
192
|
-
|
|
193
|
-
// Store correlation ID for debugging
|
|
194
|
-
console.log(`Starting request ${correlationId} to ${url}`);
|
|
195
|
-
|
|
196
|
-
const headers = {
|
|
197
|
-
...req.headers,
|
|
198
|
-
"X-Correlation-ID": correlationId,
|
|
199
|
-
"X-Request-ID": correlationId,
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
return [{ ...req, headers }, url];
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
client.useResponseMiddleware(async (response) => {
|
|
206
|
-
const correlationId = response.headers.get("X-Correlation-ID");
|
|
207
|
-
console.log(
|
|
208
|
-
`Completed request ${correlationId} with status ${response.status}`,
|
|
209
|
-
);
|
|
210
|
-
return response;
|
|
211
|
-
});
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Eventual Consistency Polling
|
|
215
|
-
|
|
216
|
-
Handle read-after-write scenarios by polling until data is available:
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
// Create a specialized client for polling operations
|
|
220
|
-
const pollingClient = new FetchClient();
|
|
221
|
-
|
|
222
|
-
pollingClient.useResponseMiddleware(async (response) => {
|
|
223
|
-
// If we get 404 on a read after write, poll until available
|
|
224
|
-
if (response.status === 404 && response.headers.get("X-Operation-ID")) {
|
|
225
|
-
const operationId = response.headers.get("X-Operation-ID");
|
|
226
|
-
const maxRetries = 10;
|
|
227
|
-
const retryDelay = 1000; // 1 second
|
|
228
|
-
|
|
229
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
230
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
const retryResponse = await fetch(response.url, {
|
|
234
|
-
method: "GET",
|
|
235
|
-
headers: { "X-Operation-ID": operationId },
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
if (retryResponse.ok) {
|
|
239
|
-
return retryResponse;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (retryResponse.status !== 404) {
|
|
243
|
-
return retryResponse; // Return other errors immediately
|
|
244
|
-
}
|
|
245
|
-
} catch (error) {
|
|
246
|
-
console.warn(`Polling attempt ${attempt + 1} failed:`, error);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return response;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Usage for read-after-write operations
|
|
255
|
-
const createUser = async (userData: any) => {
|
|
256
|
-
// Write operation
|
|
257
|
-
const createResponse = await client.post("/api/users", userData);
|
|
258
|
-
const operationId = createResponse.headers.get("X-Operation-ID");
|
|
259
|
-
|
|
260
|
-
// Read operation with polling fallback
|
|
261
|
-
const userResponse = await pollingClient.get(
|
|
262
|
-
`/api/users/${createResponse.id}`,
|
|
263
|
-
{
|
|
264
|
-
headers: { "X-Operation-ID": operationId },
|
|
265
|
-
},
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
return userResponse;
|
|
269
|
-
};
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Centralized Error Mapping
|
|
273
|
-
|
|
274
|
-
Transform backend errors into user-friendly messages:
|
|
275
|
-
|
|
276
|
-
```ts
|
|
277
|
-
// Define error mappings
|
|
278
|
-
const errorMappings = {
|
|
279
|
-
400: "Invalid request. Please check your input.",
|
|
280
|
-
401: "Please log in to continue.",
|
|
281
|
-
403: "You don't have permission to perform this action.",
|
|
282
|
-
404: "The requested resource was not found.",
|
|
283
|
-
422: "Validation failed. Please check your input.",
|
|
284
|
-
429: "Too many requests. Please try again later.",
|
|
285
|
-
500: "An internal error occurred. Please try again.",
|
|
286
|
-
502: "Service temporarily unavailable.",
|
|
287
|
-
503: "Service temporarily unavailable.",
|
|
288
|
-
504: "Request timed out. Please try again.",
|
|
289
|
-
};
|
|
290
52
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const userMessage =
|
|
297
|
-
errorMappings[response.status] || "An unexpected error occurred.";
|
|
298
|
-
|
|
299
|
-
// Add user-friendly message to error body
|
|
300
|
-
const enhancedBody = {
|
|
301
|
-
...body,
|
|
302
|
-
userMessage,
|
|
303
|
-
originalStatus: response.status,
|
|
304
|
-
timestamp: new Date().toISOString(),
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// Create a new response with enhanced error information
|
|
308
|
-
return new Response(JSON.stringify(enhancedBody), {
|
|
309
|
-
status: response.status,
|
|
310
|
-
statusText: response.statusText,
|
|
311
|
-
headers: response.headers,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return response;
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## 🔧 TypeScript Best Practices
|
|
320
|
-
|
|
321
|
-
### Typing Request and Response Shapes
|
|
322
|
-
|
|
323
|
-
Define clear interfaces for your API contracts:
|
|
324
|
-
|
|
325
|
-
```ts
|
|
326
|
-
import { FetchClient } from "@fgrzl/fetch";
|
|
327
|
-
|
|
328
|
-
// Define API response types
|
|
329
|
-
interface User {
|
|
330
|
-
id: number;
|
|
331
|
-
name: string;
|
|
332
|
-
email: string;
|
|
333
|
-
createdAt: string;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
interface CreateUserRequest {
|
|
337
|
-
name: string;
|
|
338
|
-
email: string;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
interface ApiResponse<T> {
|
|
342
|
-
data: T;
|
|
343
|
-
message: string;
|
|
344
|
-
timestamp: string;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const client = new FetchClient();
|
|
348
|
-
|
|
349
|
-
// Type-safe API calls
|
|
350
|
-
const getUser = (id: number): Promise<ApiResponse<User>> => {
|
|
351
|
-
return client.get<ApiResponse<User>>(`/api/users/${id}`);
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
const createUser = (
|
|
355
|
-
userData: CreateUserRequest,
|
|
356
|
-
): Promise<ApiResponse<User>> => {
|
|
357
|
-
return client.post<ApiResponse<User>>("/api/users", userData);
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Usage with full type safety
|
|
361
|
-
const user = await getUser(123);
|
|
362
|
-
console.log(user.data.name); // TypeScript knows this is a string
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### Generic Middleware Patterns
|
|
366
|
-
|
|
367
|
-
Create reusable, type-safe middleware:
|
|
368
|
-
|
|
369
|
-
```ts
|
|
370
|
-
import { RequestMiddleware, ResponseMiddleware } from "@fgrzl/fetch";
|
|
371
|
-
|
|
372
|
-
// Type-safe request middleware factory
|
|
373
|
-
function createAuthMiddleware<T extends string>(
|
|
374
|
-
tokenProvider: () => T | null,
|
|
375
|
-
): RequestMiddleware {
|
|
376
|
-
return async (req, url) => {
|
|
377
|
-
const token = tokenProvider();
|
|
378
|
-
const headers = {
|
|
379
|
-
...req.headers,
|
|
380
|
-
...(token && { Authorization: `Bearer ${token}` }),
|
|
381
|
-
};
|
|
382
|
-
return [{ ...req, headers }, url];
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Type-safe response middleware for data transformation
|
|
387
|
-
function createDataTransformMiddleware<TInput, TOutput>(
|
|
388
|
-
transformer: (input: TInput) => TOutput,
|
|
389
|
-
): ResponseMiddleware {
|
|
390
|
-
return async (response) => {
|
|
391
|
-
if (response.ok && response.headers.get("content-type")?.includes("json")) {
|
|
392
|
-
const data = (await response.json()) as TInput;
|
|
393
|
-
const transformedData = transformer(data);
|
|
394
|
-
|
|
395
|
-
return new Response(JSON.stringify(transformedData), {
|
|
396
|
-
status: response.status,
|
|
397
|
-
statusText: response.statusText,
|
|
398
|
-
headers: response.headers,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
return response;
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Usage
|
|
406
|
-
const authMiddleware = createAuthMiddleware(() =>
|
|
407
|
-
localStorage.getItem("token"),
|
|
408
|
-
);
|
|
409
|
-
const transformMiddleware = createDataTransformMiddleware<
|
|
410
|
-
RawApiData,
|
|
411
|
-
CleanData
|
|
412
|
-
>((raw) => ({ ...raw, processedAt: new Date() }));
|
|
413
|
-
|
|
414
|
-
client.useRequestMiddleware(authMiddleware);
|
|
415
|
-
client.useResponseMiddleware(transformMiddleware);
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
### Type-Safe Error Handling
|
|
419
|
-
|
|
420
|
-
Create typed error handlers for different scenarios:
|
|
421
|
-
|
|
422
|
-
```ts
|
|
423
|
-
import { HttpError, NetworkError, FetchError } from "@fgrzl/fetch";
|
|
424
|
-
|
|
425
|
-
// Define error types for your API
|
|
426
|
-
interface ApiError {
|
|
427
|
-
code: string;
|
|
428
|
-
message: string;
|
|
429
|
-
details?: Record<string, any>;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
interface ValidationError extends ApiError {
|
|
433
|
-
code: "VALIDATION_ERROR";
|
|
434
|
-
details: {
|
|
435
|
-
field: string;
|
|
436
|
-
message: string;
|
|
437
|
-
}[];
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Type-safe error handling utility
|
|
441
|
-
async function handleApiCall<T>(
|
|
442
|
-
apiCall: () => Promise<T>,
|
|
443
|
-
): Promise<{ data?: T; error?: string }> {
|
|
444
|
-
try {
|
|
445
|
-
const data = await apiCall();
|
|
446
|
-
return { data };
|
|
447
|
-
} catch (error) {
|
|
448
|
-
if (error instanceof HttpError) {
|
|
449
|
-
const apiError = error.body as ApiError;
|
|
450
|
-
|
|
451
|
-
switch (apiError.code) {
|
|
452
|
-
case "VALIDATION_ERROR":
|
|
453
|
-
const validationError = apiError as ValidationError;
|
|
454
|
-
return {
|
|
455
|
-
error: `Validation failed: ${validationError.details.map((d) => d.message).join(", ")}`,
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
case "UNAUTHORIZED":
|
|
459
|
-
return { error: "Please log in to continue" };
|
|
460
|
-
|
|
461
|
-
default:
|
|
462
|
-
return { error: apiError.message || "An error occurred" };
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (error instanceof NetworkError) {
|
|
467
|
-
return { error: "Network error. Please check your connection." };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return { error: "An unexpected error occurred" };
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Usage
|
|
475
|
-
const result = await handleApiCall(() => client.get<User>("/api/users/123"));
|
|
476
|
-
if (result.error) {
|
|
477
|
-
console.error(result.error);
|
|
53
|
+
// All requests now return FetchResponse<T>
|
|
54
|
+
interface User { id: number; name: string; }
|
|
55
|
+
const userResponse = await client.get<User>("/api/user");
|
|
56
|
+
if (userResponse.ok) {
|
|
57
|
+
console.log(userResponse.data.name); // Typed access to data
|
|
478
58
|
} else {
|
|
479
|
-
console.
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
## 🚀 Advanced Usage
|
|
484
|
-
|
|
485
|
-
### Conditional Middleware Application
|
|
486
|
-
|
|
487
|
-
Apply middleware only for specific routes or conditions:
|
|
488
|
-
|
|
489
|
-
```ts
|
|
490
|
-
import { RequestMiddleware, ResponseMiddleware } from "@fgrzl/fetch";
|
|
491
|
-
|
|
492
|
-
// Conditional request middleware
|
|
493
|
-
const conditionalAuthMiddleware: RequestMiddleware = async (req, url) => {
|
|
494
|
-
// Only add auth to protected routes
|
|
495
|
-
if (url.includes("/api/protected/") || url.includes("/api/admin/")) {
|
|
496
|
-
const token = localStorage.getItem("admin-token");
|
|
497
|
-
const headers = {
|
|
498
|
-
...req.headers,
|
|
499
|
-
...(token && { Authorization: `Bearer ${token}` }),
|
|
500
|
-
};
|
|
501
|
-
return [{ ...req, headers }, url];
|
|
502
|
-
}
|
|
503
|
-
return [req, url];
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
// Conditional response middleware
|
|
507
|
-
const conditionalCachingMiddleware: ResponseMiddleware = async (response) => {
|
|
508
|
-
// Only cache GET requests to specific endpoints
|
|
509
|
-
if (response.url.includes("/api/cache/") && response.status === 200) {
|
|
510
|
-
const cacheKey = `cache_${response.url}`;
|
|
511
|
-
const data = await response.clone().text();
|
|
512
|
-
localStorage.setItem(cacheKey, data);
|
|
513
|
-
localStorage.setItem(`${cacheKey}_timestamp`, Date.now().toString());
|
|
514
|
-
}
|
|
515
|
-
return response;
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
client.useRequestMiddleware(conditionalAuthMiddleware);
|
|
519
|
-
client.useResponseMiddleware(conditionalCachingMiddleware);
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
### Middleware Composition and Factories
|
|
523
|
-
|
|
524
|
-
Create composable middleware for complex scenarios:
|
|
525
|
-
|
|
526
|
-
```ts
|
|
527
|
-
// Middleware factory for different environments
|
|
528
|
-
function createEnvironmentMiddleware(environment: "dev" | "staging" | "prod") {
|
|
529
|
-
const configs = {
|
|
530
|
-
dev: { baseUrl: "http://localhost:3000", debug: true },
|
|
531
|
-
staging: { baseUrl: "https://staging-api.example.com", debug: true },
|
|
532
|
-
prod: { baseUrl: "https://api.example.com", debug: false },
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const config = configs[environment];
|
|
536
|
-
|
|
537
|
-
const requestMiddleware: RequestMiddleware = async (req, url) => {
|
|
538
|
-
// Convert relative URLs to absolute
|
|
539
|
-
const fullUrl = url.startsWith("/") ? `${config.baseUrl}${url}` : url;
|
|
540
|
-
|
|
541
|
-
const headers = {
|
|
542
|
-
...req.headers,
|
|
543
|
-
"X-Environment": environment,
|
|
544
|
-
...(config.debug && { "X-Debug": "true" }),
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
return [{ ...req, headers }, fullUrl];
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
const responseMiddleware: ResponseMiddleware = async (response) => {
|
|
551
|
-
if (config.debug) {
|
|
552
|
-
console.log(
|
|
553
|
-
`[${environment.upper()}] ${response.status} ${response.url}`,
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
return response;
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
return { requestMiddleware, responseMiddleware };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Middleware composition utility
|
|
563
|
-
function composeMiddleware(
|
|
564
|
-
...middlewares: RequestMiddleware[]
|
|
565
|
-
): RequestMiddleware {
|
|
566
|
-
return async (req, url) => {
|
|
567
|
-
let currentReq = req;
|
|
568
|
-
let currentUrl = url;
|
|
569
|
-
|
|
570
|
-
for (const middleware of middlewares) {
|
|
571
|
-
[currentReq, currentUrl] = await middleware(currentReq, currentUrl);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return [currentReq, currentUrl];
|
|
575
|
-
};
|
|
59
|
+
console.error(`Failed with status ${userResponse.status}`);
|
|
576
60
|
}
|
|
577
|
-
|
|
578
|
-
// Usage
|
|
579
|
-
const { requestMiddleware, responseMiddleware } =
|
|
580
|
-
createEnvironmentMiddleware("dev");
|
|
581
|
-
|
|
582
|
-
const composedMiddleware = composeMiddleware(
|
|
583
|
-
requestMiddleware,
|
|
584
|
-
createAuthMiddleware(() => localStorage.getItem("token")),
|
|
585
|
-
createLoggingMiddleware(),
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
client.useRequestMiddleware(composedMiddleware);
|
|
589
|
-
client.useResponseMiddleware(responseMiddleware);
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### Performance Optimizations
|
|
593
|
-
|
|
594
|
-
Optimize middleware for high-throughput applications:
|
|
595
|
-
|
|
596
|
-
```ts
|
|
597
|
-
// Cached middleware to avoid repeated computations
|
|
598
|
-
const createCachedAuthMiddleware = (): RequestMiddleware => {
|
|
599
|
-
let cachedToken: string | null = null;
|
|
600
|
-
let tokenExpiry: number = 0;
|
|
601
|
-
|
|
602
|
-
return async (req, url) => {
|
|
603
|
-
const now = Date.now();
|
|
604
|
-
|
|
605
|
-
// Refresh token only if expired
|
|
606
|
-
if (!cachedToken || now > tokenExpiry) {
|
|
607
|
-
cachedToken = localStorage.getItem("auth-token");
|
|
608
|
-
// Cache for 5 minutes
|
|
609
|
-
tokenExpiry = now + 5 * 60 * 1000;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const headers = {
|
|
613
|
-
...req.headers,
|
|
614
|
-
...(cachedToken && { Authorization: `Bearer ${cachedToken}` }),
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
return [{ ...req, headers }, url];
|
|
618
|
-
};
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// Debounced middleware for rate limiting
|
|
622
|
-
const createDebouncedMiddleware = (delay: number = 100): RequestMiddleware => {
|
|
623
|
-
const pending = new Map<string, Promise<[RequestInit, string]>>();
|
|
624
|
-
|
|
625
|
-
return async (req, url) => {
|
|
626
|
-
const key = `${req.method || "GET"}:${url}`;
|
|
627
|
-
|
|
628
|
-
if (pending.has(key)) {
|
|
629
|
-
return pending.get(key)!;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const promise = new Promise<[RequestInit, string]>((resolve) => {
|
|
633
|
-
setTimeout(() => {
|
|
634
|
-
pending.delete(key);
|
|
635
|
-
resolve([req, url]);
|
|
636
|
-
}, delay);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
pending.set(key, promise);
|
|
640
|
-
return promise;
|
|
641
|
-
};
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
// Circuit breaker pattern
|
|
645
|
-
const createCircuitBreakerMiddleware = (
|
|
646
|
-
failureThreshold: number = 5,
|
|
647
|
-
resetTimeout: number = 60000,
|
|
648
|
-
): ResponseMiddleware => {
|
|
649
|
-
let failures = 0;
|
|
650
|
-
let lastFailureTime = 0;
|
|
651
|
-
let isOpen = false;
|
|
652
|
-
|
|
653
|
-
return async (response) => {
|
|
654
|
-
const now = Date.now();
|
|
655
|
-
|
|
656
|
-
// Reset circuit if timeout has passed
|
|
657
|
-
if (isOpen && now - lastFailureTime > resetTimeout) {
|
|
658
|
-
isOpen = false;
|
|
659
|
-
failures = 0;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (isOpen) {
|
|
663
|
-
throw new Error("Circuit breaker is open");
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (!response.ok && response.status >= 500) {
|
|
667
|
-
failures++;
|
|
668
|
-
lastFailureTime = now;
|
|
669
|
-
|
|
670
|
-
if (failures >= failureThreshold) {
|
|
671
|
-
isOpen = true;
|
|
672
|
-
console.warn("Circuit breaker opened due to repeated failures");
|
|
673
|
-
}
|
|
674
|
-
} else if (response.ok) {
|
|
675
|
-
// Reset on success
|
|
676
|
-
failures = 0;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return response;
|
|
680
|
-
};
|
|
681
|
-
};
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### Complete Integration Example
|
|
685
|
-
|
|
686
|
-
Here's a complete example showing multiple patterns working together:
|
|
687
|
-
|
|
688
|
-
```ts
|
|
689
|
-
import { FetchClient, HttpError } from "@fgrzl/fetch";
|
|
690
|
-
|
|
691
|
-
// Types
|
|
692
|
-
interface ApiConfig {
|
|
693
|
-
baseUrl: string;
|
|
694
|
-
environment: "dev" | "staging" | "prod";
|
|
695
|
-
enableRetry: boolean;
|
|
696
|
-
enableCircuitBreaker: boolean;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
interface User {
|
|
700
|
-
id: number;
|
|
701
|
-
name: string;
|
|
702
|
-
email: string;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Create configured client
|
|
706
|
-
function createApiClient(config: ApiConfig): FetchClient {
|
|
707
|
-
const client = new FetchClient({
|
|
708
|
-
credentials: "same-origin",
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// Environment-specific middleware
|
|
712
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
713
|
-
const fullUrl = url.startsWith("/") ? `${config.baseUrl}${url}` : url;
|
|
714
|
-
const headers = {
|
|
715
|
-
...req.headers,
|
|
716
|
-
"Content-Type": "application/json",
|
|
717
|
-
"X-Environment": config.environment,
|
|
718
|
-
"X-Client-Version": "1.0.0",
|
|
719
|
-
};
|
|
720
|
-
return [{ ...req, headers }, fullUrl];
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
// Auth middleware
|
|
724
|
-
client.useRequestMiddleware(createCachedAuthMiddleware());
|
|
725
|
-
|
|
726
|
-
// Correlation ID middleware
|
|
727
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
728
|
-
const correlationId = crypto.randomUUID();
|
|
729
|
-
const headers = {
|
|
730
|
-
...req.headers,
|
|
731
|
-
"X-Correlation-ID": correlationId,
|
|
732
|
-
};
|
|
733
|
-
return [{ ...req, headers }, url];
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
// Circuit breaker (production only)
|
|
737
|
-
if (config.enableCircuitBreaker && config.environment === "prod") {
|
|
738
|
-
client.useResponseMiddleware(createCircuitBreakerMiddleware());
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Retry middleware
|
|
742
|
-
if (config.enableRetry) {
|
|
743
|
-
client.useResponseMiddleware(async (response) => {
|
|
744
|
-
if (response.status >= 500 && response.status < 600) {
|
|
745
|
-
// Retry logic here
|
|
746
|
-
console.log("Retrying request due to server error...");
|
|
747
|
-
}
|
|
748
|
-
return response;
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Error mapping
|
|
753
|
-
client.useResponseMiddleware(async (response) => {
|
|
754
|
-
if (!response.ok) {
|
|
755
|
-
const correlationId = response.headers.get("X-Correlation-ID");
|
|
756
|
-
console.error(`Request failed [${correlationId}]:`, response.status);
|
|
757
|
-
}
|
|
758
|
-
return response;
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
return client;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Usage
|
|
765
|
-
const apiClient = createApiClient({
|
|
766
|
-
baseUrl: "https://api.example.com",
|
|
767
|
-
environment: "prod",
|
|
768
|
-
enableRetry: true,
|
|
769
|
-
enableCircuitBreaker: true,
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
// Type-safe API methods
|
|
773
|
-
const userApi = {
|
|
774
|
-
getUser: (id: number): Promise<User> => apiClient.get<User>(`/users/${id}`),
|
|
775
|
-
|
|
776
|
-
createUser: (userData: Omit<User, "id">): Promise<User> =>
|
|
777
|
-
apiClient.post<User>("/users", userData),
|
|
778
|
-
|
|
779
|
-
updateUser: (id: number, userData: Partial<User>): Promise<User> =>
|
|
780
|
-
apiClient.put<User>(`/users/${id}`, userData),
|
|
781
|
-
|
|
782
|
-
deleteUser: (id: number): Promise<void> =>
|
|
783
|
-
apiClient.del<void>(`/users/${id}`),
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
// Usage with error handling
|
|
787
|
-
try {
|
|
788
|
-
const user = await userApi.getUser(123);
|
|
789
|
-
console.log("User loaded:", user.name);
|
|
790
|
-
} catch (error) {
|
|
791
|
-
if (error instanceof HttpError) {
|
|
792
|
-
console.error("API Error:", error.status, error.body);
|
|
793
|
-
} else {
|
|
794
|
-
console.error("Unexpected error:", error);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
## ⚡ Performance Considerations
|
|
800
|
-
|
|
801
|
-
### Middleware Order Optimization
|
|
802
|
-
|
|
803
|
-
Order middleware strategically for best performance:
|
|
804
|
-
|
|
805
|
-
```ts
|
|
806
|
-
const client = new FetchClient();
|
|
807
|
-
|
|
808
|
-
// ✅ Fast middleware first (simple header additions)
|
|
809
|
-
client.useRequestMiddleware(addCorrelationId);
|
|
810
|
-
client.useRequestMiddleware(addTimestamp);
|
|
811
|
-
|
|
812
|
-
// ✅ Medium complexity middleware
|
|
813
|
-
client.useRequestMiddleware(addAuthToken);
|
|
814
|
-
client.useRequestMiddleware(transformUrl);
|
|
815
|
-
|
|
816
|
-
// ✅ Heavy middleware last (async operations, storage access)
|
|
817
|
-
client.useRequestMiddleware(checkCacheAndModifyRequest);
|
|
818
|
-
client.useRequestMiddleware(validateAndEnrichRequest);
|
|
819
|
-
|
|
820
|
-
// Same principle for response middleware
|
|
821
|
-
client.useResponseMiddleware(logResponse); // Fast
|
|
822
|
-
client.useResponseMiddleware(extractHeaders); // Fast
|
|
823
|
-
client.useResponseMiddleware(updateCache); // Heavy
|
|
824
|
-
client.useResponseMiddleware(processComplexData); // Heavy
|
|
825
61
|
```
|
|
826
62
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
Avoid memory leaks in long-running applications:
|
|
830
|
-
|
|
831
|
-
```ts
|
|
832
|
-
// ❌ Bad: Creates closures that hold references
|
|
833
|
-
function badMiddlewareFactory() {
|
|
834
|
-
const largeData = new Array(1000000).fill("data");
|
|
835
|
-
|
|
836
|
-
return async (req, url) => {
|
|
837
|
-
// This holds reference to largeData forever
|
|
838
|
-
return [{ ...req, someData: largeData[0] }, url];
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// ✅ Good: Clean references and use weak references where appropriate
|
|
843
|
-
function goodMiddlewareFactory() {
|
|
844
|
-
return async (req, url) => {
|
|
845
|
-
// Create data only when needed
|
|
846
|
-
const necessaryData = computeNecessaryData();
|
|
847
|
-
return [{ ...req, data: necessaryData }, url];
|
|
848
|
-
};
|
|
849
|
-
}
|
|
63
|
+
---
|
|
850
64
|
|
|
851
|
-
|
|
852
|
-
const responseCache = new WeakMap<Response, any>();
|
|
65
|
+
## Documentation
|
|
853
66
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
return response;
|
|
860
|
-
};
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
### Request Batching and Caching
|
|
864
|
-
|
|
865
|
-
Implement intelligent caching to reduce network requests:
|
|
866
|
-
|
|
867
|
-
```ts
|
|
868
|
-
// Simple request deduplication
|
|
869
|
-
class RequestDeduplicator {
|
|
870
|
-
private pending = new Map<string, Promise<Response>>();
|
|
871
|
-
|
|
872
|
-
createMiddleware(): RequestMiddleware {
|
|
873
|
-
return async (req, url) => {
|
|
874
|
-
const key = `${req.method || "GET"}:${url}:${JSON.stringify(req.body)}`;
|
|
875
|
-
|
|
876
|
-
// For GET requests, deduplicate concurrent identical requests
|
|
877
|
-
if (req.method === "GET" && this.pending.has(key)) {
|
|
878
|
-
console.log("Deduplicating request:", key);
|
|
879
|
-
// Return same promise for identical concurrent requests
|
|
880
|
-
await this.pending.get(key);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return [req, url];
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
}
|
|
67
|
+
- [Project Overview](docs/overview.md)
|
|
68
|
+
- [Middleware](docs/middleware.md)
|
|
69
|
+
- [Error Handling](docs/errors.md)
|
|
70
|
+
- [Testing](docs/testing.md)
|
|
887
71
|
|
|
888
|
-
|
|
889
|
-
client.useRequestMiddleware(deduplicator.createMiddleware());
|
|
72
|
+
---
|
|
890
73
|
|
|
891
|
-
|
|
892
|
-
class ResponseCache {
|
|
893
|
-
private cache = new Map<string, { data: any; expiry: number }>();
|
|
74
|
+
## License
|
|
894
75
|
|
|
895
|
-
|
|
896
|
-
return async (response) => {
|
|
897
|
-
if (response.ok && response.url.includes("/api/cache/")) {
|
|
898
|
-
const key = response.url;
|
|
899
|
-
const now = Date.now();
|
|
900
|
-
|
|
901
|
-
// Clean expired entries
|
|
902
|
-
for (const [k, v] of this.cache.entries()) {
|
|
903
|
-
if (v.expiry < now) {
|
|
904
|
-
this.cache.delete(k);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Cache successful responses
|
|
909
|
-
const data = await response.clone().json();
|
|
910
|
-
this.cache.set(key, { data, expiry: now + ttlMs });
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return response;
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const cache = new ResponseCache();
|
|
919
|
-
client.useResponseMiddleware(cache.createMiddleware(5 * 60 * 1000)); // 5 minute TTL
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
### Monitoring and Metrics
|
|
923
|
-
|
|
924
|
-
Track performance metrics for optimization:
|
|
925
|
-
|
|
926
|
-
```ts
|
|
927
|
-
class PerformanceMonitor {
|
|
928
|
-
private metrics = {
|
|
929
|
-
requestCount: 0,
|
|
930
|
-
responseCount: 0,
|
|
931
|
-
averageResponseTime: 0,
|
|
932
|
-
errorRate: 0,
|
|
933
|
-
slowRequests: 0,
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
createRequestMiddleware(): RequestMiddleware {
|
|
937
|
-
return async (req, url) => {
|
|
938
|
-
this.metrics.requestCount++;
|
|
939
|
-
|
|
940
|
-
// Add performance marker
|
|
941
|
-
const startTime = performance.now();
|
|
942
|
-
const headers = {
|
|
943
|
-
...req.headers,
|
|
944
|
-
"X-Start-Time": startTime.toString(),
|
|
945
|
-
};
|
|
946
|
-
|
|
947
|
-
return [{ ...req, headers }, url];
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
createResponseMiddleware(): ResponseMiddleware {
|
|
952
|
-
return async (response) => {
|
|
953
|
-
this.metrics.responseCount++;
|
|
954
|
-
|
|
955
|
-
const startTime = parseFloat(response.headers.get("X-Start-Time") || "0");
|
|
956
|
-
if (startTime > 0) {
|
|
957
|
-
const responseTime = performance.now() - startTime;
|
|
958
|
-
|
|
959
|
-
// Update average response time
|
|
960
|
-
this.metrics.averageResponseTime =
|
|
961
|
-
(this.metrics.averageResponseTime + responseTime) / 2;
|
|
962
|
-
|
|
963
|
-
// Track slow requests (>2s)
|
|
964
|
-
if (responseTime > 2000) {
|
|
965
|
-
this.metrics.slowRequests++;
|
|
966
|
-
console.warn(
|
|
967
|
-
`Slow request detected: ${response.url} took ${responseTime}ms`,
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Track error rate
|
|
973
|
-
if (!response.ok) {
|
|
974
|
-
this.metrics.errorRate =
|
|
975
|
-
(this.metrics.errorRate * (this.metrics.responseCount - 1) + 1) /
|
|
976
|
-
this.metrics.responseCount;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
return response;
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
getMetrics() {
|
|
984
|
-
return { ...this.metrics };
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
reset() {
|
|
988
|
-
this.metrics = {
|
|
989
|
-
requestCount: 0,
|
|
990
|
-
responseCount: 0,
|
|
991
|
-
averageResponseTime: 0,
|
|
992
|
-
errorRate: 0,
|
|
993
|
-
slowRequests: 0,
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Usage
|
|
999
|
-
const monitor = new PerformanceMonitor();
|
|
1000
|
-
client.useRequestMiddleware(monitor.createRequestMiddleware());
|
|
1001
|
-
client.useResponseMiddleware(monitor.createResponseMiddleware());
|
|
1002
|
-
|
|
1003
|
-
// Check metrics periodically
|
|
1004
|
-
setInterval(() => {
|
|
1005
|
-
const metrics = monitor.getMetrics();
|
|
1006
|
-
console.log("API Performance:", metrics);
|
|
1007
|
-
|
|
1008
|
-
if (metrics.errorRate > 0.1) {
|
|
1009
|
-
// >10% error rate
|
|
1010
|
-
console.warn("High error rate detected!", metrics);
|
|
1011
|
-
}
|
|
1012
|
-
}, 30000); // Every 30 seconds
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
## 🔐 CSRF + 401 Handling
|
|
1016
|
-
|
|
1017
|
-
The default export is pre-configured with:
|
|
1018
|
-
|
|
1019
|
-
- `credentials: 'same-origin'`
|
|
1020
|
-
- CSRF token from `csrf_token` cookie
|
|
1021
|
-
- 401 redirect to `/login?returnTo=...`
|
|
1022
|
-
|
|
1023
|
-
## 📋 Quick Copy-Paste Examples
|
|
1024
|
-
|
|
1025
|
-
### Basic Auth Token
|
|
1026
|
-
|
|
1027
|
-
```ts
|
|
1028
|
-
import { FetchClient } from "@fgrzl/fetch";
|
|
1029
|
-
|
|
1030
|
-
const client = new FetchClient();
|
|
1031
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
1032
|
-
const token = localStorage.getItem("token");
|
|
1033
|
-
return [
|
|
1034
|
-
{
|
|
1035
|
-
...req,
|
|
1036
|
-
headers: { ...req.headers, Authorization: `Bearer ${token}` },
|
|
1037
|
-
},
|
|
1038
|
-
url,
|
|
1039
|
-
];
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
// Usage
|
|
1043
|
-
const data = await client.get("/api/protected-resource");
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
### Request Logging
|
|
1047
|
-
|
|
1048
|
-
```ts
|
|
1049
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
1050
|
-
console.log(`🚀 ${req.method || "GET"} ${url}`);
|
|
1051
|
-
return [req, url];
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
client.useResponseMiddleware(async (res) => {
|
|
1055
|
-
console.log(`✅ ${res.status} ${res.url}`);
|
|
1056
|
-
return res;
|
|
1057
|
-
});
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
### Automatic Retry
|
|
1061
|
-
|
|
1062
|
-
```ts
|
|
1063
|
-
client.useResponseMiddleware(async (response) => {
|
|
1064
|
-
if (response.status >= 500 && response.status < 600) {
|
|
1065
|
-
console.log("Retrying request...");
|
|
1066
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1067
|
-
return fetch(response.url, response);
|
|
1068
|
-
}
|
|
1069
|
-
return response;
|
|
1070
|
-
});
|
|
1071
|
-
```
|
|
1072
|
-
|
|
1073
|
-
### Error Notifications
|
|
1074
|
-
|
|
1075
|
-
```ts
|
|
1076
|
-
client.useResponseMiddleware(async (response) => {
|
|
1077
|
-
if (!response.ok) {
|
|
1078
|
-
const message = `Request failed: ${response.status} ${response.statusText}`;
|
|
1079
|
-
// Show toast notification, update UI, etc.
|
|
1080
|
-
console.error(message);
|
|
1081
|
-
}
|
|
1082
|
-
return response;
|
|
1083
|
-
});
|
|
1084
|
-
```
|
|
1085
|
-
|
|
1086
|
-
### Development Debug Headers
|
|
1087
|
-
|
|
1088
|
-
```ts
|
|
1089
|
-
if (process.env.NODE_ENV === "development") {
|
|
1090
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
1091
|
-
return [
|
|
1092
|
-
{
|
|
1093
|
-
...req,
|
|
1094
|
-
headers: {
|
|
1095
|
-
...req.headers,
|
|
1096
|
-
"X-Debug": "true",
|
|
1097
|
-
"X-Timestamp": new Date().toISOString(),
|
|
1098
|
-
"X-User-Agent": navigator.userAgent,
|
|
1099
|
-
},
|
|
1100
|
-
},
|
|
1101
|
-
url,
|
|
1102
|
-
];
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
```
|
|
1106
|
-
|
|
1107
|
-
### Simple Rate Limiting
|
|
1108
|
-
|
|
1109
|
-
```ts
|
|
1110
|
-
let lastRequest = 0;
|
|
1111
|
-
const RATE_LIMIT_MS = 1000; // 1 request per second
|
|
1112
|
-
|
|
1113
|
-
client.useRequestMiddleware(async (req, url) => {
|
|
1114
|
-
const now = Date.now();
|
|
1115
|
-
const timeSinceLastRequest = now - lastRequest;
|
|
1116
|
-
|
|
1117
|
-
if (timeSinceLastRequest < RATE_LIMIT_MS) {
|
|
1118
|
-
await new Promise((resolve) =>
|
|
1119
|
-
setTimeout(resolve, RATE_LIMIT_MS - timeSinceLastRequest),
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
lastRequest = Date.now();
|
|
1124
|
-
return [req, url];
|
|
1125
|
-
});
|
|
1126
|
-
```
|
|
1127
|
-
|
|
1128
|
-
## 🧪 Testing
|
|
1129
|
-
|
|
1130
|
-
```bash
|
|
1131
|
-
npm run test
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
## 🛠 Build
|
|
1135
|
-
|
|
1136
|
-
```bash
|
|
1137
|
-
npm run build
|
|
1138
|
-
```
|
|
76
|
+
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -13,6 +13,29 @@ export type RequestMiddleware = (req: RequestInit, url: string) => Promise<[Requ
|
|
|
13
13
|
* @returns A promise that resolves to the modified response
|
|
14
14
|
*/
|
|
15
15
|
export type ResponseMiddleware = (res: Response) => Promise<Response>;
|
|
16
|
+
/**
|
|
17
|
+
* Typed response wrapper that includes response metadata.
|
|
18
|
+
* @template T - The type of the response data
|
|
19
|
+
*/
|
|
20
|
+
export interface FetchResponse<T> {
|
|
21
|
+
/** The parsed response data */
|
|
22
|
+
data: T;
|
|
23
|
+
/** HTTP status code */
|
|
24
|
+
status: number;
|
|
25
|
+
/** HTTP status text */
|
|
26
|
+
statusText: string;
|
|
27
|
+
/** Response headers */
|
|
28
|
+
headers: Headers;
|
|
29
|
+
/** The original URL */
|
|
30
|
+
url: string;
|
|
31
|
+
/** Whether the response was successful (status 200-299) */
|
|
32
|
+
ok: boolean;
|
|
33
|
+
/** Error information if the request failed */
|
|
34
|
+
error?: {
|
|
35
|
+
message: string;
|
|
36
|
+
body?: any;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
16
39
|
/**
|
|
17
40
|
* Configuration options for FetchClient.
|
|
18
41
|
*/
|
|
@@ -65,24 +88,23 @@ export declare class FetchClient {
|
|
|
65
88
|
* @template T - The expected response type
|
|
66
89
|
* @param url - The URL to request
|
|
67
90
|
* @param init - Request configuration options
|
|
68
|
-
* @returns Promise that resolves to the
|
|
69
|
-
* @throws The error object from the response if the request fails
|
|
91
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
70
92
|
*/
|
|
71
|
-
request<T = any>(url: string, init?: RequestInit): Promise<T
|
|
93
|
+
request<T = any>(url: string, init?: RequestInit): Promise<FetchResponse<T>>;
|
|
72
94
|
/**
|
|
73
95
|
* Makes a GET request.
|
|
74
96
|
* @template T - The expected response type
|
|
75
97
|
* @param url - The URL to request
|
|
76
|
-
* @returns Promise that resolves to the
|
|
98
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
77
99
|
*/
|
|
78
|
-
get<T>(url: string): Promise<T
|
|
100
|
+
get<T>(url: string): Promise<FetchResponse<T>>;
|
|
79
101
|
/**
|
|
80
102
|
* Helper method for requests with JSON body.
|
|
81
103
|
* @template T - The expected response type
|
|
82
104
|
* @param url - The URL to request
|
|
83
105
|
* @param method - The HTTP method
|
|
84
106
|
* @param body - The request body data (will be JSON stringified)
|
|
85
|
-
* @returns Promise that resolves to the
|
|
107
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
86
108
|
*/
|
|
87
109
|
private requestWithJsonBody;
|
|
88
110
|
/**
|
|
@@ -90,22 +112,22 @@ export declare class FetchClient {
|
|
|
90
112
|
* @template T - The expected response type
|
|
91
113
|
* @param url - The URL to request
|
|
92
114
|
* @param body - The request body data (will be JSON stringified)
|
|
93
|
-
* @returns Promise that resolves to the
|
|
115
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
94
116
|
*/
|
|
95
|
-
post<T>(url: string, body?: any): Promise<T
|
|
117
|
+
post<T>(url: string, body?: any): Promise<FetchResponse<T>>;
|
|
96
118
|
/**
|
|
97
119
|
* Makes a PUT request with JSON body.
|
|
98
120
|
* @template T - The expected response type
|
|
99
121
|
* @param url - The URL to request
|
|
100
122
|
* @param body - The request body data (will be JSON stringified)
|
|
101
|
-
* @returns Promise that resolves to the
|
|
123
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
102
124
|
*/
|
|
103
|
-
put<T>(url: string, body?: any): Promise<T
|
|
125
|
+
put<T>(url: string, body?: any): Promise<FetchResponse<T>>;
|
|
104
126
|
/**
|
|
105
127
|
* Makes a DELETE request.
|
|
106
128
|
* @template T - The expected response type
|
|
107
129
|
* @param url - The URL to request
|
|
108
|
-
* @returns Promise that resolves to the
|
|
130
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
109
131
|
*/
|
|
110
|
-
del<T>(url: string): Promise<T
|
|
132
|
+
del<T>(url: string): Promise<FetchResponse<T>>;
|
|
111
133
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FetchClient = void 0;
|
|
4
|
-
const errors_1 = require("./errors");
|
|
5
4
|
/**
|
|
6
5
|
* A configurable HTTP client with middleware support.
|
|
7
6
|
*
|
|
@@ -52,8 +51,7 @@ class FetchClient {
|
|
|
52
51
|
* @template T - The expected response type
|
|
53
52
|
* @param url - The URL to request
|
|
54
53
|
* @param init - Request configuration options
|
|
55
|
-
* @returns Promise that resolves to the
|
|
56
|
-
* @throws The error object from the response if the request fails
|
|
54
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
57
55
|
*/
|
|
58
56
|
async request(url, init = {}) {
|
|
59
57
|
try {
|
|
@@ -67,19 +65,47 @@ class FetchClient {
|
|
|
67
65
|
for (const mw of this.responseMiddlewares) {
|
|
68
66
|
res = await mw(res);
|
|
69
67
|
}
|
|
68
|
+
const responseMetadata = {
|
|
69
|
+
status: res.status,
|
|
70
|
+
statusText: res.statusText,
|
|
71
|
+
headers: res.headers,
|
|
72
|
+
url: res.url,
|
|
73
|
+
ok: res.ok,
|
|
74
|
+
};
|
|
70
75
|
if (!res.ok) {
|
|
71
76
|
const body = await res.json().catch(() => ({}));
|
|
72
|
-
|
|
77
|
+
return {
|
|
78
|
+
...responseMetadata,
|
|
79
|
+
data: null,
|
|
80
|
+
error: {
|
|
81
|
+
message: res.statusText,
|
|
82
|
+
body,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
73
85
|
}
|
|
74
|
-
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
return {
|
|
88
|
+
...responseMetadata,
|
|
89
|
+
data,
|
|
90
|
+
};
|
|
75
91
|
}
|
|
76
92
|
catch (error) {
|
|
77
|
-
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
93
|
+
// Handle network errors and other exceptions
|
|
80
94
|
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
81
|
-
|
|
95
|
+
return {
|
|
96
|
+
status: 0,
|
|
97
|
+
statusText: 'Network Error',
|
|
98
|
+
headers: new Headers(),
|
|
99
|
+
url,
|
|
100
|
+
ok: false,
|
|
101
|
+
data: null,
|
|
102
|
+
error: {
|
|
103
|
+
message: 'Failed to fetch',
|
|
104
|
+
body: error,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
82
107
|
}
|
|
108
|
+
// Re-throw unexpected errors
|
|
83
109
|
throw error;
|
|
84
110
|
}
|
|
85
111
|
}
|
|
@@ -87,7 +113,7 @@ class FetchClient {
|
|
|
87
113
|
* Makes a GET request.
|
|
88
114
|
* @template T - The expected response type
|
|
89
115
|
* @param url - The URL to request
|
|
90
|
-
* @returns Promise that resolves to the
|
|
116
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
91
117
|
*/
|
|
92
118
|
get(url) {
|
|
93
119
|
return this.request(url, { method: 'GET' });
|
|
@@ -98,7 +124,7 @@ class FetchClient {
|
|
|
98
124
|
* @param url - The URL to request
|
|
99
125
|
* @param method - The HTTP method
|
|
100
126
|
* @param body - The request body data (will be JSON stringified)
|
|
101
|
-
* @returns Promise that resolves to the
|
|
127
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
102
128
|
*/
|
|
103
129
|
requestWithJsonBody(url, method, body) {
|
|
104
130
|
return this.request(url, {
|
|
@@ -111,7 +137,7 @@ class FetchClient {
|
|
|
111
137
|
* @template T - The expected response type
|
|
112
138
|
* @param url - The URL to request
|
|
113
139
|
* @param body - The request body data (will be JSON stringified)
|
|
114
|
-
* @returns Promise that resolves to the
|
|
140
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
115
141
|
*/
|
|
116
142
|
post(url, body) {
|
|
117
143
|
return this.requestWithJsonBody(url, 'POST', body ?? {});
|
|
@@ -121,7 +147,7 @@ class FetchClient {
|
|
|
121
147
|
* @template T - The expected response type
|
|
122
148
|
* @param url - The URL to request
|
|
123
149
|
* @param body - The request body data (will be JSON stringified)
|
|
124
|
-
* @returns Promise that resolves to the
|
|
150
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
125
151
|
*/
|
|
126
152
|
put(url, body) {
|
|
127
153
|
return this.requestWithJsonBody(url, 'PUT', body ?? {});
|
|
@@ -130,7 +156,7 @@ class FetchClient {
|
|
|
130
156
|
* Makes a DELETE request.
|
|
131
157
|
* @template T - The expected response type
|
|
132
158
|
* @param url - The URL to request
|
|
133
|
-
* @returns Promise that resolves to the
|
|
159
|
+
* @returns Promise that resolves to a FetchResponse containing the data and metadata
|
|
134
160
|
*/
|
|
135
161
|
del(url) {
|
|
136
162
|
return this.request(url, { method: 'DELETE' });
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ declare const api: FetchClient;
|
|
|
30
30
|
export default api;
|
|
31
31
|
export { FetchError, HttpError, NetworkError } from './errors';
|
|
32
32
|
export { FetchClient } from './client';
|
|
33
|
-
export type { RequestMiddleware, ResponseMiddleware, FetchClientConfig, } from './client';
|
|
33
|
+
export type { RequestMiddleware, ResponseMiddleware, FetchClientConfig, FetchResponse, } from './client';
|
|
34
34
|
export { useCSRF } from './csrf';
|
|
35
35
|
export { useUnauthorized } from './unauthorized';
|
|
36
36
|
export type { UnauthorizedConfig } from './unauthorized';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,qCAAuC;AACvC,iCAAiC;AACjC,iDAAiD;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,GAAG,GAAG,IAAI,oBAAW,CAAC;IAC1B,WAAW,EAAE,aAAa;CAC3B,CAAC,CAAC;AAEH,4BAA4B;AAC5B,IAAA,cAAO,EAAC,GAAG,EAAE;IACX,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,cAAc;CAC3B,CAAC,CAAC;AAEH,kCAAkC;AAClC,IAAA,8BAAe,EAAC,GAAG,EAAE;IACnB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC;AAEnB,+CAA+C;AAC/C,mCAA+D;AAAtD,oGAAA,UAAU,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,sGAAA,YAAY,OAAA;AAC5C,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,qCAAuC;AACvC,iCAAiC;AACjC,iDAAiD;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,GAAG,GAAG,IAAI,oBAAW,CAAC;IAC1B,WAAW,EAAE,aAAa;CAC3B,CAAC,CAAC;AAEH,4BAA4B;AAC5B,IAAA,cAAO,EAAC,GAAG,EAAE;IACX,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,cAAc;CAC3B,CAAC,CAAC;AAEH,kCAAkC;AAClC,IAAA,8BAAe,EAAC,GAAG,EAAE;IACnB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC;AAEnB,+CAA+C;AAC/C,mCAA+D;AAAtD,oGAAA,UAAU,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,sGAAA,YAAY,OAAA;AAC5C,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;AAOpB,+BAAiC;AAAxB,+FAAA,OAAO,OAAA;AAChB,+CAAiD;AAAxC,+GAAA,eAAe,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgrzl/fetch",
|
|
3
|
-
"version": "1.1.0-alpha.
|
|
4
|
-
"description": "A simple fetch client",
|
|
3
|
+
"version": "1.1.0-alpha.3",
|
|
4
|
+
"description": "A simple fetch client with some extra goodies",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
|
7
7
|
"typescript",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build",
|
|
29
29
|
"format": "prettier --write .",
|
|
30
30
|
"format:check": "prettier --check .",
|
|
31
|
-
"test": "vitest",
|
|
31
|
+
"test": "vitest run",
|
|
32
32
|
"test:watch": "vitest --watch",
|
|
33
33
|
"test:coverage": "vitest run --coverage",
|
|
34
34
|
"lint": "npm run format:check"
|