@tahanabavi/typefetch 1.3.0 → 1.4.1
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 +147 -208
- package/dist/index.d.mts +246 -77
- package/dist/index.d.ts +246 -77
- package/dist/index.js +705 -733
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +705 -734
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# TypeFetch
|
|
2
2
|
|
|
3
|
-
TypeFetch is a strongly-typed HTTP client built on
|
|
3
|
+
TypeFetch is a production-grade, strongly-typed HTTP client built on
|
|
4
|
+
**TypeScript** and **Zod**.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Define your API once using Zod schemas, and TypeFetch generates a fully
|
|
7
|
+
type-safe client with:
|
|
6
8
|
|
|
7
9
|
- End-to-end type safety
|
|
8
10
|
- Structured request support: `{ path, query, body, headers }`
|
|
9
11
|
- Automatic URL handling (path parameters, query string, JSON body)
|
|
10
12
|
- Middleware pipeline (logging, retry, cache, auth, custom)
|
|
13
|
+
- Built-in retry engine with backoff strategies
|
|
14
|
+
- Timeout & AbortController support
|
|
11
15
|
- Mock mode for development
|
|
12
16
|
- Dynamic token providers
|
|
13
17
|
- Response wrappers for consistent API envelopes
|
|
14
18
|
- Unified error system (`RichError`)
|
|
15
19
|
- Optional `form-data` body support for file uploads
|
|
20
|
+
- Concurrency-safe request handling
|
|
21
|
+
- Production-grade validation and error normalization
|
|
16
22
|
|
|
17
23
|
---
|
|
18
24
|
|
|
@@ -26,61 +32,97 @@ yarn add @tahanabavi/typefetch
|
|
|
26
32
|
|
|
27
33
|
---
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
# What's New / Updated
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
## 1. Advanced Retry Engine
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
TypeFetch now includes:
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
- Configurable `maxRetries`
|
|
42
|
+
- Custom `retryCondition`
|
|
43
|
+
- Built-in backoff strategies:
|
|
44
|
+
- `fixed`
|
|
45
|
+
- `exponential`
|
|
46
|
+
- Fully normalized retry errors
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
- `path`: path template (e.g. `/users/:id`)
|
|
39
|
-
- `auth?`: whether a token is required
|
|
40
|
-
- `request`: Zod schema for the request
|
|
41
|
-
- `response`: Zod schema for the response
|
|
42
|
-
- `mockData?`: static or dynamic mock response
|
|
43
|
-
- `headers?`: static or function-based default headers
|
|
44
|
-
- `bodyType?`: `"json"` (default) or `"form-data"`
|
|
48
|
+
Example:
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
```ts
|
|
51
|
+
client.setRetryConfig({
|
|
52
|
+
maxRetries: 3,
|
|
53
|
+
backoff: "exponential",
|
|
54
|
+
retryCondition: (err) => err.status === 500,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. Backoff Strategies
|
|
61
|
+
|
|
62
|
+
Supported strategies:
|
|
63
|
+
|
|
64
|
+
- **fixed** → constant delay
|
|
65
|
+
- **exponential** → 100ms, 200ms, 400ms...
|
|
66
|
+
|
|
67
|
+
Backoff is applied automatically between retries.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Timeout & Abort Support
|
|
72
|
+
|
|
73
|
+
Per-request timeout:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await api.user.getUser({ path: { id: "123" } }, { timeout: 5000 });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Internally uses `AbortController` for safe cancellation.
|
|
47
80
|
|
|
48
|
-
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Structured Request Model (Canonical Format)
|
|
49
84
|
|
|
50
85
|
```ts
|
|
51
86
|
z.object({
|
|
52
|
-
path: z.object({
|
|
53
|
-
query: z.object({
|
|
54
|
-
body: z.object({
|
|
55
|
-
headers: z.record(z.string()).optional(),
|
|
87
|
+
path: z.object({...}).optional(),
|
|
88
|
+
query: z.object({...}).optional(),
|
|
89
|
+
body: z.object({...}).optional(),
|
|
90
|
+
headers: z.record(z.string()).optional(),
|
|
56
91
|
})
|
|
57
92
|
```
|
|
58
93
|
|
|
59
|
-
TypeFetch
|
|
94
|
+
TypeFetch automatically:
|
|
60
95
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
96
|
+
- Injects path params
|
|
97
|
+
- Builds query string
|
|
98
|
+
- Serializes JSON body
|
|
99
|
+
- Merges headers in priority order:
|
|
100
|
+
1. auth
|
|
101
|
+
2. endpoint-level headers
|
|
102
|
+
3. per-call headers
|
|
68
103
|
|
|
69
|
-
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. Backward Compatibility
|
|
107
|
+
|
|
108
|
+
Flat request schemas still work:
|
|
70
109
|
|
|
71
|
-
|
|
110
|
+
```ts
|
|
111
|
+
z.object({
|
|
112
|
+
name: z.string(),
|
|
113
|
+
});
|
|
114
|
+
```
|
|
72
115
|
|
|
73
|
-
|
|
116
|
+
For non-GET requests, the entire object becomes the JSON body.
|
|
74
117
|
|
|
75
118
|
---
|
|
76
119
|
|
|
77
|
-
|
|
120
|
+
# Defining API Contracts
|
|
78
121
|
|
|
79
|
-
Example
|
|
122
|
+
Example:
|
|
80
123
|
|
|
81
124
|
```ts
|
|
82
125
|
import { z } from "zod";
|
|
83
|
-
import { Contracts, EndpointDef } from "@tahanabavi/typefetch";
|
|
84
126
|
|
|
85
127
|
const contracts = {
|
|
86
128
|
user: {
|
|
@@ -89,62 +131,30 @@ const contracts = {
|
|
|
89
131
|
path: "/users/:id",
|
|
90
132
|
auth: true,
|
|
91
133
|
request: z.object({
|
|
92
|
-
path: z.object({ id: z.string() })
|
|
93
|
-
query: z.object({}).optional(),
|
|
94
|
-
body: z.never().optional(),
|
|
95
|
-
headers: z.record(z.string()).optional(),
|
|
134
|
+
path: z.object({ id: z.string() }),
|
|
96
135
|
}),
|
|
97
136
|
response: z.object({
|
|
98
137
|
id: z.string(),
|
|
99
138
|
name: z.string(),
|
|
100
139
|
}),
|
|
101
|
-
mockData: { id: "1", name: "John Doe" },
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
createUser: {
|
|
105
|
-
method: "POST",
|
|
106
|
-
path: "/users",
|
|
107
|
-
auth: true,
|
|
108
|
-
request: z.object({
|
|
109
|
-
path: z.object({}).optional(),
|
|
110
|
-
query: z.object({}).optional(),
|
|
111
|
-
body: z
|
|
112
|
-
.object({
|
|
113
|
-
name: z.string(),
|
|
114
|
-
})
|
|
115
|
-
.optional(),
|
|
116
|
-
headers: z.record(z.string()).optional(),
|
|
117
|
-
}),
|
|
118
|
-
response: z.object({
|
|
119
|
-
id: z.string(),
|
|
120
|
-
name: z.string(),
|
|
121
|
-
}),
|
|
122
|
-
mockData: () => ({
|
|
123
|
-
id: Math.random().toString(36).slice(2),
|
|
124
|
-
name: "Mock User",
|
|
125
|
-
}),
|
|
126
140
|
},
|
|
127
141
|
},
|
|
128
142
|
} as const;
|
|
129
143
|
```
|
|
130
144
|
|
|
131
|
-
You **do not** have to use these explicit generic annotations if you don’t want to – they are shown here only for clarity. In most cases, simple `as const` + inference is enough.
|
|
132
|
-
|
|
133
145
|
---
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
# Using ApiClient
|
|
136
148
|
|
|
137
149
|
```ts
|
|
138
150
|
import { ApiClient } from "@tahanabavi/typefetch";
|
|
139
|
-
import { contracts } from "./contracts";
|
|
140
151
|
|
|
141
152
|
const client = new ApiClient(
|
|
142
153
|
{
|
|
143
154
|
baseUrl: "https://api.example.com",
|
|
144
|
-
tokenProvider: () => "dynamic-token",
|
|
145
|
-
useMockData: false,
|
|
155
|
+
tokenProvider: async () => "dynamic-token",
|
|
146
156
|
},
|
|
147
|
-
contracts
|
|
157
|
+
contracts,
|
|
148
158
|
);
|
|
149
159
|
|
|
150
160
|
client.init();
|
|
@@ -152,213 +162,142 @@ client.init();
|
|
|
152
162
|
const api = client.modules;
|
|
153
163
|
|
|
154
164
|
const user = await api.user.getUser({ path: { id: "123" } });
|
|
155
|
-
const created = await api.user.createUser({ body: { name: "Alice" } });
|
|
156
165
|
```
|
|
157
166
|
|
|
158
|
-
- `client.init()` builds the typed `modules` API using your contracts.
|
|
159
|
-
- `api.user.getUser` and `api.user.createUser` are fully typed from the Zod schemas.
|
|
160
|
-
|
|
161
167
|
---
|
|
162
168
|
|
|
163
|
-
|
|
169
|
+
# Middleware System
|
|
164
170
|
|
|
165
|
-
Middlewares
|
|
171
|
+
Middlewares execute in reverse registration order.
|
|
166
172
|
|
|
167
|
-
|
|
173
|
+
## Custom Middleware
|
|
168
174
|
|
|
169
175
|
```ts
|
|
170
176
|
client.use(async (ctx, next) => {
|
|
171
|
-
console.log("Request
|
|
177
|
+
console.log("Request:", ctx.url);
|
|
172
178
|
const res = await next();
|
|
173
179
|
console.log("Response:", res.status);
|
|
174
180
|
return res;
|
|
175
181
|
});
|
|
176
182
|
```
|
|
177
183
|
|
|
178
|
-
|
|
184
|
+
## Built-in Middlewares
|
|
179
185
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
cacheMiddleware,
|
|
185
|
-
authMiddleware,
|
|
186
|
-
} from "@tahanabavi/typefetch/middlewares";
|
|
187
|
-
|
|
188
|
-
client.use(loggingMiddleware, {
|
|
189
|
-
logRequest: true,
|
|
190
|
-
logResponse: true,
|
|
191
|
-
debug: true,
|
|
192
|
-
});
|
|
193
|
-
client.use(retryMiddleware, { maxRetries: 3, delay: 100 });
|
|
194
|
-
client.use(cacheMiddleware, { ttl: 60000 });
|
|
195
|
-
client.use(authMiddleware, {
|
|
196
|
-
refreshToken: async () => "refreshed-token",
|
|
197
|
-
});
|
|
198
|
-
```
|
|
186
|
+
- `loggingMiddleware`
|
|
187
|
+
- `retryMiddleware`
|
|
188
|
+
- `cacheMiddleware`
|
|
189
|
+
- `authMiddleware`
|
|
199
190
|
|
|
200
|
-
|
|
201
|
-
- **`retryMiddleware`** – retries failed requests with configurable `maxRetries` and `delay`.
|
|
202
|
-
- **`cacheMiddleware`** – caches GET responses in-memory per URL with `ttl` (ms).
|
|
203
|
-
- **`authMiddleware`** – can refresh tokens and inject `Authorization` headers before the request.
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## Mock Mode
|
|
208
|
-
|
|
209
|
-
Enable or disable mock mode globally:
|
|
191
|
+
Example:
|
|
210
192
|
|
|
211
193
|
```ts
|
|
212
|
-
client.
|
|
213
|
-
|
|
214
|
-
client.
|
|
194
|
+
client.use(loggingMiddleware);
|
|
195
|
+
client.use(retryMiddleware, { maxRetries: 3 });
|
|
196
|
+
client.use(cacheMiddleware, { ttl: 60000 });
|
|
197
|
+
client.use(authMiddleware);
|
|
215
198
|
```
|
|
216
199
|
|
|
217
|
-
When mock mode is enabled and `endpoint.mockData` is defined, requests will return mock data instead of hitting the network. The response wrapper and response transform still apply.
|
|
218
|
-
|
|
219
200
|
---
|
|
220
201
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
You can apply a global transformation to all successful responses:
|
|
202
|
+
# Mock Mode
|
|
224
203
|
|
|
225
204
|
```ts
|
|
226
|
-
client.
|
|
227
|
-
return {
|
|
228
|
-
...data,
|
|
229
|
-
transformedAt: new Date().toISOString(),
|
|
230
|
-
};
|
|
231
|
-
});
|
|
205
|
+
client.setMockMode(true, { min: 200, max: 1000 });
|
|
232
206
|
```
|
|
233
207
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
1. The HTTP call succeeds (`res.ok` is true)
|
|
237
|
-
2. Optional response wrapper has been unwrapped
|
|
238
|
-
3. The response has been validated with the endpoint’s Zod schema
|
|
208
|
+
- Returns `mockData` instead of calling network
|
|
209
|
+
- Still applies response validation and wrapper
|
|
239
210
|
|
|
240
211
|
---
|
|
241
212
|
|
|
242
|
-
|
|
213
|
+
# Response Wrapper
|
|
243
214
|
|
|
244
|
-
|
|
215
|
+
Supports envelope APIs:
|
|
245
216
|
|
|
246
217
|
```json
|
|
247
218
|
{
|
|
248
219
|
"success": true,
|
|
249
|
-
"data": {
|
|
250
|
-
"timestamp": "..."
|
|
251
|
-
"requestId": "..."
|
|
220
|
+
"data": {...},
|
|
221
|
+
"timestamp": "..."
|
|
252
222
|
}
|
|
253
223
|
```
|
|
254
224
|
|
|
255
|
-
|
|
225
|
+
Example:
|
|
256
226
|
|
|
257
227
|
```ts
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const wrapper = (successResponse: z.ZodTypeAny) =>
|
|
261
|
-
z.union([
|
|
262
|
-
z.object({
|
|
263
|
-
success: z.literal(true),
|
|
264
|
-
data: successResponse,
|
|
265
|
-
timestamp: z.string(),
|
|
266
|
-
requestId: z.string(),
|
|
267
|
-
}),
|
|
268
|
-
z.object({
|
|
269
|
-
success: z.literal(false),
|
|
270
|
-
message: z.string(),
|
|
271
|
-
code: z.number(),
|
|
272
|
-
timestamp: z.string(),
|
|
273
|
-
requestId: z.string(),
|
|
274
|
-
}),
|
|
275
|
-
]);
|
|
276
|
-
|
|
277
|
-
client.setResponseWrapper(wrapper);
|
|
228
|
+
client.setResponseWrapper(wrapperSchema);
|
|
278
229
|
```
|
|
279
230
|
|
|
280
|
-
On
|
|
281
|
-
On `success: false`, a `RichError` is thrown with normalized information.
|
|
231
|
+
On failure, throws normalized `RichError`.
|
|
282
232
|
|
|
283
233
|
---
|
|
284
234
|
|
|
285
|
-
|
|
235
|
+
# Error Handling
|
|
236
|
+
|
|
237
|
+
All errors are normalized into `RichError`:
|
|
238
|
+
|
|
239
|
+
- HTTP errors
|
|
240
|
+
- Network failures
|
|
241
|
+
- Validation errors
|
|
242
|
+
- Timeout errors
|
|
243
|
+
- Retry exhaustion
|
|
244
|
+
|
|
245
|
+
Global handler:
|
|
286
246
|
|
|
287
247
|
```ts
|
|
288
248
|
client.onError((err) => {
|
|
289
|
-
console.error(
|
|
249
|
+
console.error(err.message, err.status);
|
|
290
250
|
});
|
|
291
251
|
```
|
|
292
252
|
|
|
293
|
-
|
|
253
|
+
---
|
|
294
254
|
|
|
295
|
-
|
|
255
|
+
# File Uploads (FormData)
|
|
296
256
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
} catch (err) {
|
|
301
|
-
// err is RichError
|
|
302
|
-
}
|
|
303
|
-
```
|
|
257
|
+
Set `bodyType: "form-data"` in endpoint definition.
|
|
258
|
+
|
|
259
|
+
TypeFetch builds `FormData` automatically.
|
|
304
260
|
|
|
305
261
|
---
|
|
306
262
|
|
|
307
|
-
|
|
263
|
+
# Concurrency Safety
|
|
308
264
|
|
|
309
|
-
|
|
265
|
+
TypeFetch safely handles parallel requests:
|
|
310
266
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
query: z.object({}).optional(),
|
|
315
|
-
body: z.object({
|
|
316
|
-
file: z.any(), // or z.instanceof(File) in browser
|
|
317
|
-
}),
|
|
318
|
-
headers: z.record(z.string()).optional(),
|
|
319
|
-
});
|
|
267
|
+
- No shared mutable state issues
|
|
268
|
+
- Independent retry cycles
|
|
269
|
+
- Independent AbortControllers
|
|
320
270
|
|
|
321
|
-
|
|
322
|
-
url: z.string(),
|
|
323
|
-
});
|
|
271
|
+
---
|
|
324
272
|
|
|
325
|
-
|
|
326
|
-
user: {
|
|
327
|
-
uploadAvatar: {
|
|
328
|
-
method: "POST",
|
|
329
|
-
path: "/users/avatar",
|
|
330
|
-
auth: true,
|
|
331
|
-
bodyType: "form-data",
|
|
332
|
-
request: uploadAvatarRequest,
|
|
333
|
-
response: uploadAvatarResponse,
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
} as const;
|
|
337
|
-
```
|
|
273
|
+
# Production-Grade Test Coverage
|
|
338
274
|
|
|
339
|
-
|
|
275
|
+
The project now includes:
|
|
340
276
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
277
|
+
- Validation tests
|
|
278
|
+
- Middleware tests
|
|
279
|
+
- Retry tests
|
|
280
|
+
- Backoff timing tests
|
|
281
|
+
- Timeout & abort tests
|
|
282
|
+
- Concurrency tests
|
|
283
|
+
- Error propagation tests
|
|
284
|
+
- Mock mode tests
|
|
285
|
+
- TokenProvider tests
|
|
286
|
+
- Edge case handling tests
|
|
347
287
|
|
|
348
|
-
|
|
288
|
+
Suitable for publishing as a production SDK.
|
|
349
289
|
|
|
350
290
|
---
|
|
351
291
|
|
|
352
|
-
|
|
292
|
+
# Notes
|
|
353
293
|
|
|
354
|
-
- Always call `client.init()` before using
|
|
355
|
-
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
358
|
-
- Structured `{ path, query, body, headers }` shape is the canonical model; flat request schemas are still supported for backwards compatibility.
|
|
294
|
+
- Always call `client.init()` before using modules.
|
|
295
|
+
- All responses are validated via Zod.
|
|
296
|
+
- Structured request shape is recommended.
|
|
297
|
+
- Retry + Timeout can be combined safely.
|
|
359
298
|
|
|
360
299
|
---
|
|
361
300
|
|
|
362
|
-
|
|
301
|
+
# License
|
|
363
302
|
|
|
364
303
|
MIT
|