@fgrzl/fetch 1.1.0-alpha.2 → 1.1.0-alpha.7
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 +31 -1090
- package/dist/cjs/client/fetch-client.d.ts +189 -0
- package/dist/cjs/client/fetch-client.d.ts.map +1 -0
- package/dist/cjs/client/fetch-client.js +339 -0
- package/dist/cjs/client/fetch-client.js.map +1 -0
- package/dist/cjs/client/index.d.ts +11 -0
- package/dist/cjs/client/index.d.ts.map +1 -0
- package/dist/cjs/client/index.js +14 -0
- package/dist/cjs/client/index.js.map +1 -0
- package/dist/cjs/client/types.d.ts +63 -0
- package/dist/cjs/client/types.d.ts.map +1 -0
- package/dist/cjs/client/types.js +9 -0
- package/dist/cjs/client/types.js.map +1 -0
- package/dist/{errors.d.ts → cjs/errors/index.d.ts} +20 -3
- package/dist/cjs/errors/index.d.ts.map +1 -0
- package/dist/{errors.js → cjs/errors/index.js} +23 -3
- package/dist/cjs/errors/index.js.map +1 -0
- package/dist/cjs/index.d.ts +65 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +118 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/middleware/authentication/authentication.d.ts +31 -0
- package/dist/cjs/middleware/authentication/authentication.d.ts.map +1 -0
- package/dist/cjs/middleware/authentication/authentication.js +93 -0
- package/dist/cjs/middleware/authentication/authentication.js.map +1 -0
- package/dist/cjs/middleware/authentication/index.d.ts +37 -0
- package/dist/cjs/middleware/authentication/index.d.ts.map +1 -0
- package/dist/cjs/middleware/authentication/index.js +42 -0
- package/dist/cjs/middleware/authentication/index.js.map +1 -0
- package/dist/cjs/middleware/authentication/types.d.ts +73 -0
- package/dist/cjs/middleware/authentication/types.d.ts.map +1 -0
- package/dist/cjs/middleware/authentication/types.js +6 -0
- package/dist/cjs/middleware/authentication/types.js.map +1 -0
- package/dist/cjs/middleware/authorization/authorization.d.ts +30 -0
- package/dist/cjs/middleware/authorization/authorization.d.ts.map +1 -0
- package/dist/cjs/middleware/authorization/authorization.js +82 -0
- package/dist/cjs/middleware/authorization/authorization.js.map +1 -0
- package/dist/cjs/middleware/authorization/index.d.ts +36 -0
- package/dist/cjs/middleware/authorization/index.d.ts.map +1 -0
- package/dist/cjs/middleware/authorization/index.js +41 -0
- package/dist/cjs/middleware/authorization/index.js.map +1 -0
- package/dist/cjs/middleware/authorization/types.d.ts +67 -0
- package/dist/cjs/middleware/authorization/types.d.ts.map +1 -0
- package/dist/cjs/middleware/authorization/types.js +6 -0
- package/dist/cjs/middleware/authorization/types.js.map +1 -0
- package/dist/cjs/middleware/cache/cache.d.ts +41 -0
- package/dist/cjs/middleware/cache/cache.d.ts.map +1 -0
- package/dist/cjs/middleware/cache/cache.js +191 -0
- package/dist/cjs/middleware/cache/cache.js.map +1 -0
- package/dist/cjs/middleware/cache/index.d.ts +44 -0
- package/dist/cjs/middleware/cache/index.d.ts.map +1 -0
- package/dist/cjs/middleware/cache/index.js +50 -0
- package/dist/cjs/middleware/cache/index.js.map +1 -0
- package/dist/cjs/middleware/cache/types.d.ts +89 -0
- package/dist/cjs/middleware/cache/types.d.ts.map +1 -0
- package/dist/cjs/middleware/cache/types.js +6 -0
- package/dist/cjs/middleware/cache/types.js.map +1 -0
- package/dist/cjs/middleware/csrf/csrf.d.ts +34 -0
- package/dist/cjs/middleware/csrf/csrf.d.ts.map +1 -0
- package/dist/cjs/middleware/csrf/csrf.js +94 -0
- package/dist/cjs/middleware/csrf/csrf.js.map +1 -0
- package/dist/cjs/middleware/csrf/index.d.ts +57 -0
- package/dist/cjs/middleware/csrf/index.d.ts.map +1 -0
- package/dist/cjs/middleware/csrf/index.js +62 -0
- package/dist/cjs/middleware/csrf/index.js.map +1 -0
- package/dist/cjs/middleware/csrf/types.d.ts +57 -0
- package/dist/cjs/middleware/csrf/types.d.ts.map +1 -0
- package/dist/cjs/middleware/csrf/types.js +6 -0
- package/dist/cjs/middleware/csrf/types.js.map +1 -0
- package/dist/cjs/middleware/index.d.ts +115 -0
- package/dist/cjs/middleware/index.d.ts.map +1 -0
- package/dist/cjs/middleware/index.js +153 -0
- package/dist/cjs/middleware/index.js.map +1 -0
- package/dist/cjs/middleware/logging/index.d.ts +42 -0
- package/dist/cjs/middleware/logging/index.d.ts.map +1 -0
- package/dist/cjs/middleware/logging/index.js +47 -0
- package/dist/cjs/middleware/logging/index.js.map +1 -0
- package/dist/cjs/middleware/logging/logging.d.ts +29 -0
- package/dist/cjs/middleware/logging/logging.d.ts.map +1 -0
- package/dist/cjs/middleware/logging/logging.js +171 -0
- package/dist/cjs/middleware/logging/logging.js.map +1 -0
- package/dist/cjs/middleware/logging/types.d.ts +90 -0
- package/dist/cjs/middleware/logging/types.d.ts.map +1 -0
- package/dist/cjs/middleware/logging/types.js +6 -0
- package/dist/cjs/middleware/logging/types.js.map +1 -0
- package/dist/cjs/middleware/rate-limit/index.d.ts +16 -0
- package/dist/cjs/middleware/rate-limit/index.d.ts.map +1 -0
- package/dist/cjs/middleware/rate-limit/index.js +21 -0
- package/dist/cjs/middleware/rate-limit/index.js.map +1 -0
- package/dist/cjs/middleware/rate-limit/rate-limit.d.ts +14 -0
- package/dist/cjs/middleware/rate-limit/rate-limit.d.ts.map +1 -0
- package/dist/cjs/middleware/rate-limit/rate-limit.js +87 -0
- package/dist/cjs/middleware/rate-limit/rate-limit.js.map +1 -0
- package/dist/cjs/middleware/rate-limit/types.d.ts +97 -0
- package/dist/cjs/middleware/rate-limit/types.d.ts.map +1 -0
- package/dist/cjs/middleware/rate-limit/types.js +6 -0
- package/dist/cjs/middleware/rate-limit/types.js.map +1 -0
- package/dist/cjs/middleware/retry/index.d.ts +6 -0
- package/dist/cjs/middleware/retry/index.d.ts.map +1 -0
- package/dist/cjs/middleware/retry/index.js +11 -0
- package/dist/cjs/middleware/retry/index.js.map +1 -0
- package/dist/cjs/middleware/retry/retry.d.ts +39 -0
- package/dist/cjs/middleware/retry/retry.d.ts.map +1 -0
- package/dist/cjs/middleware/retry/retry.js +144 -0
- package/dist/cjs/middleware/retry/retry.js.map +1 -0
- package/dist/cjs/middleware/retry/types.d.ts +61 -0
- package/dist/cjs/middleware/retry/types.d.ts.map +1 -0
- package/dist/cjs/middleware/retry/types.js +6 -0
- package/dist/cjs/middleware/retry/types.js.map +1 -0
- package/dist/client/fetch-client.d.ts +189 -0
- package/dist/client/fetch-client.d.ts.map +1 -0
- package/dist/client/fetch-client.js +335 -0
- package/dist/client/fetch-client.js.map +1 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +10 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +63 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +8 -0
- package/dist/client/types.js.map +1 -0
- package/dist/errors/index.d.ts +64 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +73 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +49 -20
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -42
- package/dist/index.js.map +1 -1
- package/dist/middleware/authentication/authentication.d.ts +31 -0
- package/dist/middleware/authentication/authentication.d.ts.map +1 -0
- package/dist/middleware/authentication/authentication.js +90 -0
- package/dist/middleware/authentication/authentication.js.map +1 -0
- package/dist/middleware/authentication/index.d.ts +37 -0
- package/dist/middleware/authentication/index.d.ts.map +1 -0
- package/dist/middleware/authentication/index.js +37 -0
- package/dist/middleware/authentication/index.js.map +1 -0
- package/dist/middleware/authentication/types.d.ts +73 -0
- package/dist/middleware/authentication/types.d.ts.map +1 -0
- package/dist/middleware/authentication/types.js +5 -0
- package/dist/middleware/authentication/types.js.map +1 -0
- package/dist/middleware/authorization/authorization.d.ts +30 -0
- package/dist/middleware/authorization/authorization.d.ts.map +1 -0
- package/dist/middleware/authorization/authorization.js +79 -0
- package/dist/middleware/authorization/authorization.js.map +1 -0
- package/dist/middleware/authorization/index.d.ts +36 -0
- package/dist/middleware/authorization/index.d.ts.map +1 -0
- package/dist/middleware/authorization/index.js +36 -0
- package/dist/middleware/authorization/index.js.map +1 -0
- package/dist/middleware/authorization/types.d.ts +67 -0
- package/dist/middleware/authorization/types.d.ts.map +1 -0
- package/dist/middleware/authorization/types.js +5 -0
- package/dist/middleware/authorization/types.js.map +1 -0
- package/dist/middleware/cache/cache.d.ts +41 -0
- package/dist/middleware/cache/cache.d.ts.map +1 -0
- package/dist/middleware/cache/cache.js +186 -0
- package/dist/middleware/cache/cache.js.map +1 -0
- package/dist/middleware/cache/index.d.ts +44 -0
- package/dist/middleware/cache/index.d.ts.map +1 -0
- package/dist/middleware/cache/index.js +44 -0
- package/dist/middleware/cache/index.js.map +1 -0
- package/dist/middleware/cache/types.d.ts +89 -0
- package/dist/middleware/cache/types.d.ts.map +1 -0
- package/dist/middleware/cache/types.js +5 -0
- package/dist/middleware/cache/types.js.map +1 -0
- package/dist/middleware/csrf/csrf.d.ts +34 -0
- package/dist/middleware/csrf/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf/csrf.js +91 -0
- package/dist/middleware/csrf/csrf.js.map +1 -0
- package/dist/middleware/csrf/index.d.ts +57 -0
- package/dist/middleware/csrf/index.d.ts.map +1 -0
- package/dist/middleware/csrf/index.js +57 -0
- package/dist/middleware/csrf/index.js.map +1 -0
- package/dist/middleware/csrf/types.d.ts +57 -0
- package/dist/middleware/csrf/types.d.ts.map +1 -0
- package/dist/middleware/csrf/types.js +5 -0
- package/dist/middleware/csrf/types.js.map +1 -0
- package/dist/middleware/index.d.ts +115 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +134 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logging/index.d.ts +42 -0
- package/dist/middleware/logging/index.d.ts.map +1 -0
- package/dist/middleware/logging/index.js +42 -0
- package/dist/middleware/logging/index.js.map +1 -0
- package/dist/middleware/logging/logging.d.ts +29 -0
- package/dist/middleware/logging/logging.d.ts.map +1 -0
- package/dist/middleware/logging/logging.js +168 -0
- package/dist/middleware/logging/logging.js.map +1 -0
- package/dist/middleware/logging/types.d.ts +90 -0
- package/dist/middleware/logging/types.d.ts.map +1 -0
- package/dist/middleware/logging/types.js +5 -0
- package/dist/middleware/logging/types.js.map +1 -0
- package/dist/middleware/rate-limit/index.d.ts +16 -0
- package/dist/middleware/rate-limit/index.d.ts.map +1 -0
- package/dist/middleware/rate-limit/index.js +16 -0
- package/dist/middleware/rate-limit/index.js.map +1 -0
- package/dist/middleware/rate-limit/rate-limit.d.ts +14 -0
- package/dist/middleware/rate-limit/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit/rate-limit.js +84 -0
- package/dist/middleware/rate-limit/rate-limit.js.map +1 -0
- package/dist/middleware/rate-limit/types.d.ts +97 -0
- package/dist/middleware/rate-limit/types.d.ts.map +1 -0
- package/dist/middleware/rate-limit/types.js +5 -0
- package/dist/middleware/rate-limit/types.js.map +1 -0
- package/dist/middleware/retry/index.d.ts +6 -0
- package/dist/middleware/retry/index.d.ts.map +1 -0
- package/dist/middleware/retry/index.js +6 -0
- package/dist/middleware/retry/index.js.map +1 -0
- package/dist/middleware/retry/retry.d.ts +39 -0
- package/dist/middleware/retry/retry.d.ts.map +1 -0
- package/dist/middleware/retry/retry.js +141 -0
- package/dist/middleware/retry/retry.js.map +1 -0
- package/dist/middleware/retry/types.d.ts +61 -0
- package/dist/middleware/retry/types.d.ts.map +1 -0
- package/dist/middleware/retry/types.js +5 -0
- package/dist/middleware/retry/types.js.map +1 -0
- package/package.json +43 -9
- package/dist/client.d.ts +0 -111
- package/dist/client.js +0 -140
- package/dist/client.js.map +0 -1
- package/dist/csrf.d.ts +0 -32
- package/dist/csrf.js +0 -53
- package/dist/csrf.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/test-utils.d.ts +0 -24
- package/dist/test-utils.js +0 -52
- package/dist/test-utils.js.map +0 -1
- package/dist/unauthorized.d.ts +0 -27
- package/dist/unauthorized.js +0 -41
- package/dist/unauthorized.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,9 +7,11 @@ A lightweight, middleware-friendly fetch client for TypeScript projects.
|
|
|
7
7
|
|
|
8
8
|
## ✨ Features
|
|
9
9
|
|
|
10
|
+
- **Pit of Success Design**: Simple defaults that just work, customizable when needed
|
|
10
11
|
- Simple API: `api.get('/api/user')`
|
|
11
|
-
- Built-in CSRF token support
|
|
12
|
+
- Built-in CSRF token support (XSRF-TOKEN standard)
|
|
12
13
|
- Automatic 401 redirect handling
|
|
14
|
+
- Retry middleware with configurable strategies
|
|
13
15
|
- Custom middleware support (request/response)
|
|
14
16
|
- TypeScript-first, small and dependency-free
|
|
15
17
|
|
|
@@ -21,1118 +23,57 @@ npm install @fgrzl/fetch
|
|
|
21
23
|
|
|
22
24
|
## 🚀 Quick Start
|
|
23
25
|
|
|
26
|
+
**Level 1: Just works with defaults**
|
|
27
|
+
|
|
24
28
|
```ts
|
|
25
29
|
import api from "@fgrzl/fetch";
|
|
26
30
|
|
|
27
|
-
const
|
|
31
|
+
const response = await api.get("/api/user");
|
|
32
|
+
if (response.ok) {
|
|
33
|
+
console.log(response.data); // Your typed data
|
|
34
|
+
} else {
|
|
35
|
+
console.error(`Error ${response.status}:`, response.error?.message);
|
|
36
|
+
}
|
|
28
37
|
```
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
**Level 2: Custom configuration when needed**
|
|
31
40
|
|
|
32
41
|
```ts
|
|
33
|
-
import { FetchClient, useCSRF,
|
|
42
|
+
import { FetchClient, useCSRF, useAuthorization, useRetry } from "@fgrzl/fetch";
|
|
34
43
|
|
|
35
44
|
const client = new FetchClient({
|
|
36
45
|
credentials: "same-origin",
|
|
37
46
|
});
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
useUnauthorized(client, {
|
|
45
|
-
loginPath: "/login",
|
|
46
|
-
});
|
|
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:
|
|
48
|
+
// Smart defaults - just works
|
|
49
|
+
useCSRF(client);
|
|
50
|
+
useAuthorization(client);
|
|
51
|
+
useRetry(client);
|
|
275
52
|
|
|
276
|
-
|
|
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
|
|
53
|
+
// All requests now return FetchResponse<T>
|
|
329
54
|
interface User {
|
|
330
55
|
id: number;
|
|
331
56
|
name: string;
|
|
332
|
-
email: string;
|
|
333
|
-
createdAt: string;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
interface CreateUserRequest {
|
|
337
|
-
name: string;
|
|
338
|
-
email: string;
|
|
339
57
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
data
|
|
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);
|
|
58
|
+
const userResponse = await client.get<User>("/api/user");
|
|
59
|
+
if (userResponse.ok) {
|
|
60
|
+
console.log(userResponse.data.name); // Typed access to data
|
|
478
61
|
} else {
|
|
479
|
-
console.
|
|
62
|
+
console.error(`Failed with status ${userResponse.status}`);
|
|
480
63
|
}
|
|
481
64
|
```
|
|
482
65
|
|
|
483
|
-
|
|
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
|
-
};
|
|
66
|
+
---
|
|
549
67
|
|
|
550
|
-
|
|
551
|
-
if (config.debug) {
|
|
552
|
-
console.log(
|
|
553
|
-
`[${environment.upper()}] ${response.status} ${response.url}`,
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
return response;
|
|
557
|
-
};
|
|
68
|
+
## Documentation
|
|
558
69
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
```
|
|
70
|
+
- [Project Overview](docs/overview.md)
|
|
71
|
+
- [Middleware](docs/middleware.md)
|
|
72
|
+
- [Error Handling](docs/errors.md)
|
|
73
|
+
- [Testing](docs/testing.md)
|
|
591
74
|
|
|
592
|
-
|
|
75
|
+
---
|
|
593
76
|
|
|
594
|
-
|
|
77
|
+
## License
|
|
595
78
|
|
|
596
|
-
|
|
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
|
-
```
|
|
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
|
|
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
|
-
```
|
|
79
|
+
MIT
|