@fgrzl/fetch 0.1.0-alpha.27 โ†’ 0.1.0-alpha.28

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.
Files changed (2) hide show
  1. package/README.md +1064 -3
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -48,12 +48,968 @@ useUnauthorized(client, {
48
48
 
49
49
  ## ๐Ÿงฉ Middleware
50
50
 
51
- Add your own middleware:
51
+ ### Request Middleware
52
+
53
+ Request middleware functions run before the HTTP request is sent, allowing you to modify request options and URLs.
52
54
 
53
55
  ```ts
56
+ import { FetchClient, RequestMiddleware } from "@fgrzl/fetch";
57
+
58
+ const client = new FetchClient();
59
+
60
+ // Add authentication header
54
61
  client.useRequestMiddleware(async (req, url) => {
55
- return [{ ...req, headers: { ...req.headers, "X-Debug": "true" } }, 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
+
291
+ client.useResponseMiddleware(async (response) => {
292
+ if (!response.ok) {
293
+ const body = await response.json().catch(() => ({}));
294
+
295
+ // Create user-friendly error with mapped message
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);
478
+ } else {
479
+ console.log(result.data.name); // TypeScript knows data is User
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
+ };
576
+ }
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,
56
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
+ ```
826
+
827
+ ### Memory Management
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
+ }
850
+
851
+ // โœ… Good: Use WeakMap for temporary caching
852
+ const responseCache = new WeakMap<Response, any>();
853
+
854
+ const cachingMiddleware: ResponseMiddleware = async (response) => {
855
+ if (!responseCache.has(response)) {
856
+ const data = await response.clone().json();
857
+ responseCache.set(response, data);
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
+ }
887
+
888
+ const deduplicator = new RequestDeduplicator();
889
+ client.useRequestMiddleware(deduplicator.createMiddleware());
890
+
891
+ // Response caching with TTL
892
+ class ResponseCache {
893
+ private cache = new Map<string, { data: any; expiry: number }>();
894
+
895
+ createMiddleware(ttlMs: number = 300000): ResponseMiddleware {
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
57
1013
  ```
58
1014
 
59
1015
  ## ๐Ÿ” CSRF + 401 Handling
@@ -61,9 +1017,114 @@ client.useRequestMiddleware(async (req, url) => {
61
1017
  The default export is pre-configured with:
62
1018
 
63
1019
  - `credentials: 'same-origin'`
64
- - CSRF token from `XSRF-TOKEN` cookie
1020
+ - CSRF token from `csrf_token` cookie
65
1021
  - 401 redirect to `/login?returnTo=...`
66
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
+
67
1128
  ## ๐Ÿงช Testing
68
1129
 
69
1130
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgrzl/fetch",
3
- "version": "0.1.0-alpha.27",
3
+ "version": "0.1.0-alpha.28",
4
4
  "description": "A simple fetch client",
5
5
  "keywords": [
6
6
  "fetch",