@tahanabavi/typefetch 1.4.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1025 -138
- package/dist/index.d.mts +97 -69
- package/dist/index.d.ts +97 -69
- package/dist/index.js +430 -4138
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +418 -4144
- package/dist/index.mjs.map +1 -1
- package/package.json +49 -15
package/README.md
CHANGED
|
@@ -1,137 +1,302 @@
|
|
|
1
1
|
# TypeFetch
|
|
2
2
|
|
|
3
|
-
TypeFetch is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
**TypeFetch** is a strongly typed HTTP client for TypeScript projects, built around **Zod** contracts.
|
|
4
|
+
|
|
5
|
+
Define your API once with Zod schemas, then TypeFetch generates a fully typed client with request validation, response validation, middleware support, retries, mock data, response wrappers, token handling, and structured request support.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const user = await api.user.getUser({
|
|
9
|
+
path: { id: "123" },
|
|
10
|
+
});
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
* End-to-end TypeScript inference from Zod schemas
|
|
18
|
+
* Runtime request and response validation
|
|
19
|
+
* Structured request model: `{ path, query, body, headers }`
|
|
20
|
+
* Automatic path parameter injection
|
|
21
|
+
* Automatic query string generation
|
|
22
|
+
* JSON and `form-data` request bodies
|
|
23
|
+
* Middleware pipeline
|
|
24
|
+
* Built-in retry engine with backoff strategies
|
|
25
|
+
* Timeout and `AbortController` support
|
|
26
|
+
* Static tokens and dynamic token providers
|
|
27
|
+
* Mock mode for development and testing
|
|
28
|
+
* Response wrapper support for API envelopes
|
|
29
|
+
* Normalized error handling with `RichError`
|
|
30
|
+
* Field-level encryption middleware
|
|
31
|
+
* Backward-compatible flat request schemas
|
|
22
32
|
|
|
23
33
|
---
|
|
24
34
|
|
|
25
35
|
## Installation
|
|
26
36
|
|
|
27
37
|
```bash
|
|
28
|
-
npm install @tahanabavi/typefetch
|
|
29
|
-
# or
|
|
30
|
-
yarn add @tahanabavi/typefetch
|
|
38
|
+
npm install @tahanabavi/typefetch zod
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
Or with Yarn:
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
```bash
|
|
44
|
+
yarn add @tahanabavi/typefetch zod
|
|
45
|
+
```
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
Or with pnpm:
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @tahanabavi/typefetch zod
|
|
51
|
+
```
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
- Custom `retryCondition`
|
|
43
|
-
- Built-in backoff strategies:
|
|
44
|
-
- `fixed`
|
|
45
|
-
- `exponential`
|
|
46
|
-
- Fully normalized retry errors
|
|
53
|
+
---
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
## Quick Start
|
|
49
56
|
|
|
50
57
|
```ts
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
backoff: "exponential",
|
|
54
|
-
retryCondition: (err) => err.status === 500,
|
|
55
|
-
});
|
|
56
|
-
```
|
|
58
|
+
import { z } from "zod";
|
|
59
|
+
import { ApiClient } from "@tahanabavi/typefetch";
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
const contracts = {
|
|
62
|
+
user: {
|
|
63
|
+
getUser: {
|
|
64
|
+
method: "GET",
|
|
65
|
+
path: "/users/:id",
|
|
66
|
+
auth: true,
|
|
67
|
+
request: z.object({
|
|
68
|
+
path: z.object({
|
|
69
|
+
id: z.string(),
|
|
70
|
+
}),
|
|
71
|
+
}),
|
|
72
|
+
response: z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
name: z.string(),
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
const client = new ApiClient(
|
|
81
|
+
{
|
|
82
|
+
baseUrl: "https://api.example.com",
|
|
83
|
+
tokenProvider: async () => "your-token",
|
|
84
|
+
},
|
|
85
|
+
contracts,
|
|
86
|
+
);
|
|
59
87
|
|
|
60
|
-
|
|
88
|
+
client.init();
|
|
61
89
|
|
|
62
|
-
|
|
90
|
+
const api = client.modules;
|
|
63
91
|
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
const user = await api.user.getUser({
|
|
93
|
+
path: { id: "123" },
|
|
94
|
+
});
|
|
66
95
|
|
|
67
|
-
|
|
96
|
+
console.log(user.name);
|
|
97
|
+
```
|
|
68
98
|
|
|
69
99
|
---
|
|
70
100
|
|
|
71
|
-
##
|
|
101
|
+
## Defining API Contracts
|
|
72
102
|
|
|
73
|
-
|
|
103
|
+
A TypeFetch contract is a grouped object of modules and endpoints.
|
|
74
104
|
|
|
75
105
|
```ts
|
|
76
|
-
|
|
106
|
+
const contracts = {
|
|
107
|
+
user: {
|
|
108
|
+
getUser: {
|
|
109
|
+
method: "GET",
|
|
110
|
+
path: "/users/:id",
|
|
111
|
+
request: z.object({
|
|
112
|
+
path: z.object({
|
|
113
|
+
id: z.string(),
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
response: z.object({
|
|
117
|
+
id: z.string(),
|
|
118
|
+
name: z.string(),
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
createUser: {
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: "/users",
|
|
125
|
+
request: z.object({
|
|
126
|
+
body: z.object({
|
|
127
|
+
name: z.string(),
|
|
128
|
+
email: z.string().email(),
|
|
129
|
+
}),
|
|
130
|
+
}),
|
|
131
|
+
response: z.object({
|
|
132
|
+
id: z.string(),
|
|
133
|
+
name: z.string(),
|
|
134
|
+
email: z.string(),
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
} as const;
|
|
77
139
|
```
|
|
78
140
|
|
|
79
|
-
|
|
141
|
+
After calling `client.init()`, TypeFetch generates typed methods:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
await api.user.getUser({
|
|
145
|
+
path: { id: "123" },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await api.user.createUser({
|
|
149
|
+
body: {
|
|
150
|
+
name: "Taha",
|
|
151
|
+
email: "taha@example.com",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
80
155
|
|
|
81
156
|
---
|
|
82
157
|
|
|
83
|
-
##
|
|
158
|
+
## Structured Request Model
|
|
159
|
+
|
|
160
|
+
The recommended request shape is:
|
|
84
161
|
|
|
85
162
|
```ts
|
|
86
163
|
z.object({
|
|
87
|
-
path: z.object({
|
|
88
|
-
query: z.object({
|
|
89
|
-
body: z.
|
|
164
|
+
path: z.object({}).optional(),
|
|
165
|
+
query: z.object({}).optional(),
|
|
166
|
+
body: z.any().optional(),
|
|
90
167
|
headers: z.record(z.string()).optional(),
|
|
91
|
-
})
|
|
168
|
+
});
|
|
92
169
|
```
|
|
93
170
|
|
|
94
|
-
|
|
171
|
+
Each section has a specific purpose.
|
|
95
172
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
3. per-call headers
|
|
173
|
+
| Key | Purpose |
|
|
174
|
+
| --------- | ------------------------------------------ |
|
|
175
|
+
| `path` | Replaces path parameters like `/users/:id` |
|
|
176
|
+
| `query` | Builds the query string |
|
|
177
|
+
| `body` | Sent as the JSON or `form-data` body |
|
|
178
|
+
| `headers` | Per-request headers |
|
|
103
179
|
|
|
104
|
-
|
|
180
|
+
Example:
|
|
105
181
|
|
|
106
|
-
|
|
182
|
+
```ts
|
|
183
|
+
const contracts = {
|
|
184
|
+
user: {
|
|
185
|
+
updateUser: {
|
|
186
|
+
method: "PATCH",
|
|
187
|
+
path: "/users/:id",
|
|
188
|
+
request: z.object({
|
|
189
|
+
path: z.object({
|
|
190
|
+
id: z.string(),
|
|
191
|
+
}),
|
|
192
|
+
query: z.object({
|
|
193
|
+
notify: z.boolean().optional(),
|
|
194
|
+
}).optional(),
|
|
195
|
+
body: z.object({
|
|
196
|
+
name: z.string(),
|
|
197
|
+
}),
|
|
198
|
+
headers: z.record(z.string()).optional(),
|
|
199
|
+
}),
|
|
200
|
+
response: z.object({
|
|
201
|
+
id: z.string(),
|
|
202
|
+
name: z.string(),
|
|
203
|
+
}),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
} as const;
|
|
207
|
+
```
|
|
107
208
|
|
|
108
|
-
|
|
209
|
+
Usage:
|
|
109
210
|
|
|
110
211
|
```ts
|
|
111
|
-
|
|
112
|
-
|
|
212
|
+
await api.user.updateUser({
|
|
213
|
+
path: { id: "123" },
|
|
214
|
+
query: { notify: true },
|
|
215
|
+
headers: {
|
|
216
|
+
"X-Tenant": "main",
|
|
217
|
+
},
|
|
218
|
+
body: {
|
|
219
|
+
name: "Taha",
|
|
220
|
+
},
|
|
113
221
|
});
|
|
114
222
|
```
|
|
115
223
|
|
|
116
|
-
|
|
224
|
+
TypeFetch sends:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
PATCH /users/123?notify=true
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
With body:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"name": "Taha"
|
|
235
|
+
}
|
|
236
|
+
```
|
|
117
237
|
|
|
118
238
|
---
|
|
119
239
|
|
|
120
|
-
|
|
240
|
+
## Request Schema Helper
|
|
121
241
|
|
|
122
|
-
|
|
242
|
+
You can use `makeRequestSchema` to make structured request schemas easier to write.
|
|
123
243
|
|
|
124
244
|
```ts
|
|
125
245
|
import { z } from "zod";
|
|
246
|
+
import { makeRequestSchema } from "@tahanabavi/typefetch";
|
|
247
|
+
|
|
248
|
+
const updateUserRequest = makeRequestSchema<
|
|
249
|
+
{ id: z.ZodString },
|
|
250
|
+
{ notify: z.ZodOptional<z.ZodBoolean> },
|
|
251
|
+
z.ZodObject<{
|
|
252
|
+
name: z.ZodString;
|
|
253
|
+
}>
|
|
254
|
+
>()({
|
|
255
|
+
path: z.object({
|
|
256
|
+
id: z.string(),
|
|
257
|
+
}),
|
|
258
|
+
query: z.object({
|
|
259
|
+
notify: z.boolean().optional(),
|
|
260
|
+
}),
|
|
261
|
+
body: z.object({
|
|
262
|
+
name: z.string(),
|
|
263
|
+
}),
|
|
264
|
+
headers: z.record(z.string()).optional(),
|
|
265
|
+
});
|
|
266
|
+
```
|
|
126
267
|
|
|
268
|
+
Use it inside an endpoint:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
127
271
|
const contracts = {
|
|
128
272
|
user: {
|
|
129
|
-
|
|
130
|
-
method: "
|
|
273
|
+
updateUser: {
|
|
274
|
+
method: "PATCH",
|
|
131
275
|
path: "/users/:id",
|
|
132
|
-
|
|
276
|
+
request: updateUserRequest,
|
|
277
|
+
response: z.object({
|
|
278
|
+
id: z.string(),
|
|
279
|
+
name: z.string(),
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
} as const;
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Backward Compatibility
|
|
289
|
+
|
|
290
|
+
Flat request schemas are still supported.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const contracts = {
|
|
294
|
+
user: {
|
|
295
|
+
createUser: {
|
|
296
|
+
method: "POST",
|
|
297
|
+
path: "/users",
|
|
133
298
|
request: z.object({
|
|
134
|
-
|
|
299
|
+
name: z.string(),
|
|
135
300
|
}),
|
|
136
301
|
response: z.object({
|
|
137
302
|
id: z.string(),
|
|
@@ -142,9 +307,21 @@ const contracts = {
|
|
|
142
307
|
} as const;
|
|
143
308
|
```
|
|
144
309
|
|
|
310
|
+
Usage:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
await api.user.createUser({
|
|
314
|
+
name: "Taha",
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
For non-`GET` requests, the full flat input is sent as the JSON body.
|
|
319
|
+
|
|
320
|
+
For `GET` requests, flat input is validated but no body is sent.
|
|
321
|
+
|
|
145
322
|
---
|
|
146
323
|
|
|
147
|
-
|
|
324
|
+
## Creating the Client
|
|
148
325
|
|
|
149
326
|
```ts
|
|
150
327
|
import { ApiClient } from "@tahanabavi/typefetch";
|
|
@@ -152,7 +329,6 @@ import { ApiClient } from "@tahanabavi/typefetch";
|
|
|
152
329
|
const client = new ApiClient(
|
|
153
330
|
{
|
|
154
331
|
baseUrl: "https://api.example.com",
|
|
155
|
-
tokenProvider: async () => "dynamic-token",
|
|
156
332
|
},
|
|
157
333
|
contracts,
|
|
158
334
|
);
|
|
@@ -160,144 +336,855 @@ const client = new ApiClient(
|
|
|
160
336
|
client.init();
|
|
161
337
|
|
|
162
338
|
const api = client.modules;
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Always call `client.init()` before using `client.modules`.
|
|
163
342
|
|
|
164
|
-
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Client Configuration
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
const client = new ApiClient(
|
|
349
|
+
{
|
|
350
|
+
baseUrl: "https://api.example.com",
|
|
351
|
+
token: "static-token",
|
|
352
|
+
tokenProvider: async () => "dynamic-token",
|
|
353
|
+
useMockData: false,
|
|
354
|
+
mockDelay: {
|
|
355
|
+
min: 200,
|
|
356
|
+
max: 1000,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
contracts,
|
|
360
|
+
);
|
|
165
361
|
```
|
|
166
362
|
|
|
363
|
+
| Option | Type | Description |
|
|
364
|
+
| --------------- | --------------------------------- | ---------------------- |
|
|
365
|
+
| `baseUrl` | `string` | Base API URL |
|
|
366
|
+
| `token` | `string` | Static bearer token |
|
|
367
|
+
| `tokenProvider` | `() => string \| Promise<string>` | Dynamic token provider |
|
|
368
|
+
| `useMockData` | `boolean` | Enables mock mode |
|
|
369
|
+
| `mockDelay` | `{ min: number; max: number }` | Simulated mock latency |
|
|
370
|
+
|
|
371
|
+
When both `token` and `tokenProvider` are provided, `tokenProvider` takes priority.
|
|
372
|
+
|
|
167
373
|
---
|
|
168
374
|
|
|
169
|
-
|
|
375
|
+
## Authentication
|
|
170
376
|
|
|
171
|
-
|
|
377
|
+
Set `auth: true` on endpoints that require an authorization token.
|
|
172
378
|
|
|
173
|
-
|
|
379
|
+
```ts
|
|
380
|
+
const contracts = {
|
|
381
|
+
user: {
|
|
382
|
+
getProfile: {
|
|
383
|
+
method: "GET",
|
|
384
|
+
path: "/profile",
|
|
385
|
+
auth: true,
|
|
386
|
+
request: z.object({}),
|
|
387
|
+
response: z.object({
|
|
388
|
+
id: z.string(),
|
|
389
|
+
name: z.string(),
|
|
390
|
+
}),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
} as const;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Use a static token:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
const client = new ApiClient(
|
|
400
|
+
{
|
|
401
|
+
baseUrl: "https://api.example.com",
|
|
402
|
+
token: "my-token",
|
|
403
|
+
},
|
|
404
|
+
contracts,
|
|
405
|
+
);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Or use a dynamic token provider:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
const client = new ApiClient(
|
|
412
|
+
{
|
|
413
|
+
baseUrl: "https://api.example.com",
|
|
414
|
+
tokenProvider: async () => {
|
|
415
|
+
return localStorage.getItem("token") ?? "";
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
contracts,
|
|
419
|
+
);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
You can also set the token provider later:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
client.setTokenProvider(async () => "new-token");
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Middleware System
|
|
431
|
+
|
|
432
|
+
TypeFetch supports middleware for logging, authentication, caching, retries, encryption, and custom request behavior.
|
|
174
433
|
|
|
175
434
|
```ts
|
|
176
435
|
client.use(async (ctx, next) => {
|
|
177
436
|
console.log("Request:", ctx.url);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
437
|
+
|
|
438
|
+
const response = await next();
|
|
439
|
+
|
|
440
|
+
console.log("Response:", response.status);
|
|
441
|
+
|
|
442
|
+
return response;
|
|
181
443
|
});
|
|
182
444
|
```
|
|
183
445
|
|
|
446
|
+
Middlewares run in registration order before the request, then unwind in reverse order after the response.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
client.use(firstMiddleware);
|
|
450
|
+
client.use(secondMiddleware);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Execution flow:
|
|
454
|
+
|
|
455
|
+
```txt
|
|
456
|
+
firstMiddleware before
|
|
457
|
+
secondMiddleware before
|
|
458
|
+
fetch
|
|
459
|
+
secondMiddleware after
|
|
460
|
+
firstMiddleware after
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
184
465
|
## Built-in Middlewares
|
|
185
466
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
467
|
+
Depending on how you export your middlewares, they can be registered directly or as factories.
|
|
468
|
+
|
|
469
|
+
Direct middleware example:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
client.use(loggingMiddleware, {
|
|
473
|
+
debug: true,
|
|
474
|
+
logRequest: true,
|
|
475
|
+
logResponse: true,
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Factory middleware example:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
client.use(cacheMiddleware({ ttl: 60_000 }));
|
|
483
|
+
client.use(retryMiddleware({ maxRetries: 3, delay: 300 }));
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Retry Engine
|
|
489
|
+
|
|
490
|
+
TypeFetch includes a built-in retry engine on the client.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
client.setRetryConfig({
|
|
494
|
+
maxRetries: 3,
|
|
495
|
+
backoff: "exponential",
|
|
496
|
+
retryCondition: (error, attempt) => {
|
|
497
|
+
return error.status !== undefined && error.status >= 500;
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Supported backoff strategies:
|
|
503
|
+
|
|
504
|
+
| Strategy | Behavior |
|
|
505
|
+
| ------------- | ------------------------------ |
|
|
506
|
+
| `fixed` | Same delay each retry |
|
|
507
|
+
| `linear` | Delay increases linearly |
|
|
508
|
+
| `exponential` | Delay doubles after each retry |
|
|
190
509
|
|
|
191
510
|
Example:
|
|
192
511
|
|
|
193
512
|
```ts
|
|
194
|
-
client.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
513
|
+
client.setRetryConfig({
|
|
514
|
+
maxRetries: 3,
|
|
515
|
+
backoff: "fixed",
|
|
516
|
+
});
|
|
198
517
|
```
|
|
199
518
|
|
|
200
519
|
---
|
|
201
520
|
|
|
202
|
-
|
|
521
|
+
## Timeout and Abort Support
|
|
522
|
+
|
|
523
|
+
Each request can receive per-call options.
|
|
203
524
|
|
|
204
525
|
```ts
|
|
205
|
-
|
|
526
|
+
await api.user.getUser(
|
|
527
|
+
{
|
|
528
|
+
path: { id: "123" },
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
timeout: 5000,
|
|
532
|
+
},
|
|
533
|
+
);
|
|
206
534
|
```
|
|
207
535
|
|
|
208
|
-
|
|
209
|
-
|
|
536
|
+
You can also pass an external `AbortSignal`.
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
const controller = new AbortController();
|
|
540
|
+
|
|
541
|
+
await api.user.getUser(
|
|
542
|
+
{
|
|
543
|
+
path: { id: "123" },
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
signal: controller.signal,
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
controller.abort();
|
|
551
|
+
```
|
|
210
552
|
|
|
211
553
|
---
|
|
212
554
|
|
|
213
|
-
|
|
555
|
+
## Mock Mode
|
|
214
556
|
|
|
215
|
-
|
|
557
|
+
Mock mode lets you return endpoint-level mock data instead of calling the network.
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
const contracts = {
|
|
561
|
+
user: {
|
|
562
|
+
getUser: {
|
|
563
|
+
method: "GET",
|
|
564
|
+
path: "/users/:id",
|
|
565
|
+
request: z.object({
|
|
566
|
+
path: z.object({
|
|
567
|
+
id: z.string(),
|
|
568
|
+
}),
|
|
569
|
+
}),
|
|
570
|
+
response: z.object({
|
|
571
|
+
id: z.string(),
|
|
572
|
+
name: z.string(),
|
|
573
|
+
}),
|
|
574
|
+
mockData: {
|
|
575
|
+
id: "mock-1",
|
|
576
|
+
name: "Mock User",
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
} as const;
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Enable mock mode:
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
client.setMockMode(true, {
|
|
587
|
+
min: 200,
|
|
588
|
+
max: 1000,
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Dynamic mock data is also supported:
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
mockData: () => ({
|
|
596
|
+
id: crypto.randomUUID(),
|
|
597
|
+
name: "Dynamic Mock User",
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Mock responses are still validated against the endpoint response schema.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## Response Wrappers
|
|
606
|
+
|
|
607
|
+
Many APIs return wrapped responses.
|
|
216
608
|
|
|
217
609
|
```json
|
|
218
610
|
{
|
|
219
611
|
"success": true,
|
|
220
|
-
"data": {
|
|
221
|
-
|
|
612
|
+
"data": {
|
|
613
|
+
"id": "123",
|
|
614
|
+
"name": "Taha"
|
|
615
|
+
},
|
|
616
|
+
"timestamp": "2026-01-01T00:00:00.000Z"
|
|
222
617
|
}
|
|
223
618
|
```
|
|
224
619
|
|
|
620
|
+
TypeFetch can validate and unwrap these responses.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
import { z } from "zod";
|
|
624
|
+
|
|
625
|
+
client.setResponseWrapper((successResponse) =>
|
|
626
|
+
z.union([
|
|
627
|
+
z.object({
|
|
628
|
+
success: z.literal(true),
|
|
629
|
+
data: successResponse,
|
|
630
|
+
timestamp: z.string().optional(),
|
|
631
|
+
requestId: z.string().optional(),
|
|
632
|
+
}),
|
|
633
|
+
z.object({
|
|
634
|
+
success: z.literal(false),
|
|
635
|
+
message: z.string(),
|
|
636
|
+
code: z.number().optional(),
|
|
637
|
+
timestamp: z.string().optional(),
|
|
638
|
+
requestId: z.string().optional(),
|
|
639
|
+
}),
|
|
640
|
+
]),
|
|
641
|
+
);
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Successful responses return only `data`.
|
|
645
|
+
|
|
646
|
+
Failed wrapped responses throw `RichError`.
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Error Handling
|
|
651
|
+
|
|
652
|
+
TypeFetch normalizes errors into `RichError`.
|
|
653
|
+
|
|
654
|
+
```ts
|
|
655
|
+
client.onError((error) => {
|
|
656
|
+
console.error(error.message);
|
|
657
|
+
console.error(error.status);
|
|
658
|
+
console.error(error.code);
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
`RichError` may include:
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
{
|
|
666
|
+
message: string;
|
|
667
|
+
status?: number;
|
|
668
|
+
code?: string;
|
|
669
|
+
title?: string;
|
|
670
|
+
detail?: string;
|
|
671
|
+
errors?: Record<string, string[]>;
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Handled error types include:
|
|
676
|
+
|
|
677
|
+
* HTTP errors
|
|
678
|
+
* Validation errors
|
|
679
|
+
* Wrapped API errors
|
|
680
|
+
* Missing token errors
|
|
681
|
+
* Network errors
|
|
682
|
+
* Timeout errors
|
|
683
|
+
* Retry exhaustion
|
|
684
|
+
|
|
225
685
|
Example:
|
|
226
686
|
|
|
227
687
|
```ts
|
|
228
|
-
|
|
688
|
+
try {
|
|
689
|
+
await api.user.getUser({
|
|
690
|
+
path: { id: "missing" },
|
|
691
|
+
});
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (error instanceof RichError) {
|
|
694
|
+
console.error(error.status, error.message);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## File Uploads
|
|
702
|
+
|
|
703
|
+
Set `bodyType: "form-data"` on an endpoint.
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
const contracts = {
|
|
707
|
+
user: {
|
|
708
|
+
uploadAvatar: {
|
|
709
|
+
method: "POST",
|
|
710
|
+
path: "/users/:id/avatar",
|
|
711
|
+
bodyType: "form-data",
|
|
712
|
+
request: z.object({
|
|
713
|
+
path: z.object({
|
|
714
|
+
id: z.string(),
|
|
715
|
+
}),
|
|
716
|
+
body: z.object({
|
|
717
|
+
file: z.instanceof(File),
|
|
718
|
+
alt: z.string().optional(),
|
|
719
|
+
}),
|
|
720
|
+
}),
|
|
721
|
+
response: z.object({
|
|
722
|
+
uploaded: z.boolean(),
|
|
723
|
+
}),
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
} as const;
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Usage:
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
await api.user.uploadAvatar({
|
|
733
|
+
path: { id: "123" },
|
|
734
|
+
body: {
|
|
735
|
+
file,
|
|
736
|
+
alt: "Profile avatar",
|
|
737
|
+
},
|
|
738
|
+
});
|
|
229
739
|
```
|
|
230
740
|
|
|
231
|
-
|
|
741
|
+
When using `form-data`, TypeFetch does not force the `Content-Type: application/json` header.
|
|
232
742
|
|
|
233
743
|
---
|
|
234
744
|
|
|
235
|
-
|
|
745
|
+
## Encryption Middleware
|
|
746
|
+
|
|
747
|
+
TypeFetch includes optional field-level encryption through `encryptionMiddleware`.
|
|
748
|
+
|
|
749
|
+
It can:
|
|
750
|
+
|
|
751
|
+
* Encrypt selected request body fields
|
|
752
|
+
* Decrypt selected response fields
|
|
753
|
+
* Process deeply nested objects
|
|
754
|
+
* Process arrays
|
|
755
|
+
* Use different encryption methods per field
|
|
756
|
+
* Support custom encryption and decryption handlers
|
|
757
|
+
|
|
758
|
+
Supported methods:
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
type EncryptionMethod = "AES" | "DES" | "RSA" | "Base64" | "Custom";
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Registering the Middleware
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
import { encryptionMiddleware } from "@tahanabavi/typefetch/middlewares";
|
|
768
|
+
|
|
769
|
+
client.use(encryptionMiddleware, {
|
|
770
|
+
keyProvider: async () => ({
|
|
771
|
+
type: "symmetric",
|
|
772
|
+
key: "my-secret-key",
|
|
773
|
+
}),
|
|
774
|
+
});
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Endpoint Encryption Config
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
const contracts = {
|
|
781
|
+
secure: {
|
|
782
|
+
createSecret: {
|
|
783
|
+
method: "POST",
|
|
784
|
+
path: "/secure",
|
|
785
|
+
request: z.object({
|
|
786
|
+
body: z.object({
|
|
787
|
+
secret: z.string(),
|
|
788
|
+
profile: z.object({
|
|
789
|
+
pin: z.string(),
|
|
790
|
+
}),
|
|
791
|
+
}),
|
|
792
|
+
}),
|
|
793
|
+
response: z.object({
|
|
794
|
+
id: z.string(),
|
|
795
|
+
token: z.string(),
|
|
796
|
+
}),
|
|
797
|
+
encryption: {
|
|
798
|
+
method: "AES",
|
|
799
|
+
request: {
|
|
800
|
+
secret: true,
|
|
801
|
+
profile: {
|
|
802
|
+
pin: "Base64",
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
response: {
|
|
806
|
+
token: true,
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
} as const;
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Usage:
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
await api.secure.createSecret({
|
|
818
|
+
body: {
|
|
819
|
+
secret: "private-value",
|
|
820
|
+
profile: {
|
|
821
|
+
pin: "1234",
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Before the request is sent:
|
|
828
|
+
|
|
829
|
+
* `secret` is encrypted with AES
|
|
830
|
+
* `profile.pin` is encoded with Base64
|
|
831
|
+
|
|
832
|
+
After the response is received:
|
|
236
833
|
|
|
237
|
-
|
|
834
|
+
* `token` is decrypted with AES
|
|
238
835
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
836
|
+
### Separate Request and Response Methods
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
encryption: {
|
|
840
|
+
method: {
|
|
841
|
+
request: "RSA",
|
|
842
|
+
response: "AES",
|
|
843
|
+
},
|
|
844
|
+
request: {
|
|
845
|
+
password: true,
|
|
846
|
+
},
|
|
847
|
+
response: {
|
|
848
|
+
token: true,
|
|
849
|
+
},
|
|
850
|
+
}
|
|
851
|
+
```
|
|
244
852
|
|
|
245
|
-
|
|
853
|
+
### Custom Encryption
|
|
246
854
|
|
|
247
855
|
```ts
|
|
248
|
-
client.
|
|
249
|
-
|
|
856
|
+
client.use(encryptionMiddleware, {
|
|
857
|
+
keyProvider: async () => ({
|
|
858
|
+
type: "symmetric",
|
|
859
|
+
key: "custom-key",
|
|
860
|
+
}),
|
|
861
|
+
customHandlers: {
|
|
862
|
+
encrypt: async (value, key) => {
|
|
863
|
+
return `encrypted:${value}`;
|
|
864
|
+
},
|
|
865
|
+
decrypt: async (value, key) => {
|
|
866
|
+
return value.replace("encrypted:", "");
|
|
867
|
+
},
|
|
868
|
+
},
|
|
250
869
|
});
|
|
251
870
|
```
|
|
252
871
|
|
|
872
|
+
Endpoint config:
|
|
873
|
+
|
|
874
|
+
```ts
|
|
875
|
+
encryption: {
|
|
876
|
+
method: "Custom",
|
|
877
|
+
request: {
|
|
878
|
+
secret: true,
|
|
879
|
+
},
|
|
880
|
+
response: {
|
|
881
|
+
token: true,
|
|
882
|
+
},
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Fail-Closed Behavior
|
|
887
|
+
|
|
888
|
+
By default, encryption should fail closed.
|
|
889
|
+
|
|
890
|
+
That means if request encryption fails, the request is not sent as plaintext.
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
client.use(encryptionMiddleware, {
|
|
894
|
+
keyProvider: async () => ({
|
|
895
|
+
type: "symmetric",
|
|
896
|
+
key: "secret",
|
|
897
|
+
}),
|
|
898
|
+
failClosed: true,
|
|
899
|
+
});
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
For debugging, you may disable fail-closed behavior:
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
client.use(encryptionMiddleware, {
|
|
906
|
+
keyProvider: async () => ({
|
|
907
|
+
type: "symmetric",
|
|
908
|
+
key: "secret",
|
|
909
|
+
}),
|
|
910
|
+
failClosed: false,
|
|
911
|
+
});
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Use `failClosed: false` carefully.
|
|
915
|
+
|
|
253
916
|
---
|
|
254
917
|
|
|
255
|
-
|
|
918
|
+
## Encryption Maps
|
|
919
|
+
|
|
920
|
+
Encryption maps describe which fields should be transformed.
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
encryption: {
|
|
924
|
+
method: "AES",
|
|
925
|
+
request: {
|
|
926
|
+
password: true,
|
|
927
|
+
profile: {
|
|
928
|
+
ssn: true,
|
|
929
|
+
},
|
|
930
|
+
metadata: {
|
|
931
|
+
publicValue: false,
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
}
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
Map values:
|
|
938
|
+
|
|
939
|
+
| Value | Behavior |
|
|
940
|
+
| ---------- | ------------------------------------ |
|
|
941
|
+
| `true` | Encrypt/decrypt using default method |
|
|
942
|
+
| `false` | Skip field |
|
|
943
|
+
| `"AES"` | Use AES for this field |
|
|
944
|
+
| `"DES"` | Use DES for this field |
|
|
945
|
+
| `"RSA"` | Use RSA for this field |
|
|
946
|
+
| `"Base64"` | Use Base64 for this field |
|
|
947
|
+
| `"Custom"` | Use custom handler |
|
|
948
|
+
| `{ ... }` | Recursively process object |
|
|
949
|
+
| `[ ... ]` | Process array items |
|
|
256
950
|
|
|
257
|
-
|
|
951
|
+
Array example:
|
|
952
|
+
|
|
953
|
+
```ts
|
|
954
|
+
encryption: {
|
|
955
|
+
method: "AES",
|
|
956
|
+
request: {
|
|
957
|
+
users: [
|
|
958
|
+
{
|
|
959
|
+
password: true,
|
|
960
|
+
},
|
|
961
|
+
],
|
|
962
|
+
},
|
|
963
|
+
}
|
|
964
|
+
```
|
|
258
965
|
|
|
259
|
-
|
|
966
|
+
This applies the first array map to every item unless an index-specific map exists.
|
|
260
967
|
|
|
261
968
|
---
|
|
262
969
|
|
|
263
|
-
|
|
970
|
+
## Endpoint-Level Headers
|
|
971
|
+
|
|
972
|
+
Headers can be defined on the endpoint.
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
const contracts = {
|
|
976
|
+
user: {
|
|
977
|
+
createUser: {
|
|
978
|
+
method: "POST",
|
|
979
|
+
path: "/users",
|
|
980
|
+
request: z.object({
|
|
981
|
+
body: z.object({
|
|
982
|
+
name: z.string(),
|
|
983
|
+
}),
|
|
984
|
+
}),
|
|
985
|
+
response: z.object({
|
|
986
|
+
id: z.string(),
|
|
987
|
+
name: z.string(),
|
|
988
|
+
}),
|
|
989
|
+
headers: {
|
|
990
|
+
"X-App": "typefetch",
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
} as const;
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
Headers can also be generated from input:
|
|
998
|
+
|
|
999
|
+
```ts
|
|
1000
|
+
headers: (input) => ({
|
|
1001
|
+
"X-Tenant": input.headers?.["X-Tenant"] ?? "default",
|
|
1002
|
+
});
|
|
1003
|
+
```
|
|
264
1004
|
|
|
265
|
-
|
|
1005
|
+
Per-request headers can be passed through the structured request input:
|
|
266
1006
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
1007
|
+
```ts
|
|
1008
|
+
await api.user.createUser({
|
|
1009
|
+
headers: {
|
|
1010
|
+
"X-Request-ID": "req-123",
|
|
1011
|
+
},
|
|
1012
|
+
body: {
|
|
1013
|
+
name: "Taha",
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
```
|
|
270
1017
|
|
|
271
1018
|
---
|
|
272
1019
|
|
|
273
|
-
|
|
1020
|
+
## Custom Middleware
|
|
1021
|
+
|
|
1022
|
+
A middleware receives:
|
|
274
1023
|
|
|
275
|
-
|
|
1024
|
+
```ts
|
|
1025
|
+
type Middleware = (
|
|
1026
|
+
ctx: MiddlewareContext,
|
|
1027
|
+
next: () => Promise<Response>,
|
|
1028
|
+
options?: unknown,
|
|
1029
|
+
) => Promise<Response>;
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Example:
|
|
276
1033
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
1034
|
+
```ts
|
|
1035
|
+
client.use(async (ctx, next) => {
|
|
1036
|
+
const startedAt = Date.now();
|
|
1037
|
+
|
|
1038
|
+
const response = await next();
|
|
1039
|
+
|
|
1040
|
+
console.log(`${ctx.init.method} ${ctx.url} took ${Date.now() - startedAt}ms`);
|
|
1041
|
+
|
|
1042
|
+
return response;
|
|
1043
|
+
});
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
With options:
|
|
1047
|
+
|
|
1048
|
+
```ts
|
|
1049
|
+
const timingMiddleware = async (ctx, next, options) => {
|
|
1050
|
+
const response = await next();
|
|
1051
|
+
|
|
1052
|
+
if (options?.debug) {
|
|
1053
|
+
console.log("Timing middleware enabled");
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return response;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
client.use(timingMiddleware, {
|
|
1060
|
+
debug: true,
|
|
1061
|
+
});
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Type Inference
|
|
1067
|
+
|
|
1068
|
+
TypeFetch infers endpoint input and output types automatically from Zod schemas.
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
const user = await api.user.getUser({
|
|
1072
|
+
path: {
|
|
1073
|
+
id: "123",
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
`user` is inferred as:
|
|
1079
|
+
|
|
1080
|
+
```ts
|
|
1081
|
+
{
|
|
1082
|
+
id: string;
|
|
1083
|
+
name: string;
|
|
1084
|
+
}
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
Invalid input fails at compile time when possible and at runtime through Zod validation.
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
## Recommended Project Structure
|
|
1092
|
+
|
|
1093
|
+
```txt
|
|
1094
|
+
src/
|
|
1095
|
+
api/
|
|
1096
|
+
contracts.ts
|
|
1097
|
+
client.ts
|
|
1098
|
+
middlewares/
|
|
1099
|
+
logging.ts
|
|
1100
|
+
retry.ts
|
|
1101
|
+
cache.ts
|
|
1102
|
+
auth.ts
|
|
1103
|
+
encryption.ts
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Example:
|
|
1107
|
+
|
|
1108
|
+
```ts
|
|
1109
|
+
// api/client.ts
|
|
1110
|
+
import { ApiClient } from "@tahanabavi/typefetch";
|
|
1111
|
+
import { contracts } from "./contracts";
|
|
1112
|
+
|
|
1113
|
+
export const client = new ApiClient(
|
|
1114
|
+
{
|
|
1115
|
+
baseUrl: import.meta.env.VITE_API_URL,
|
|
1116
|
+
tokenProvider: async () => localStorage.getItem("token") ?? "",
|
|
1117
|
+
},
|
|
1118
|
+
contracts,
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
client.init();
|
|
1122
|
+
|
|
1123
|
+
export const api = client.modules;
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
---
|
|
1127
|
+
|
|
1128
|
+
## Testing
|
|
1129
|
+
|
|
1130
|
+
TypeFetch is designed to be easy to test with mocked `fetch`.
|
|
1131
|
+
|
|
1132
|
+
Example:
|
|
1133
|
+
|
|
1134
|
+
```ts
|
|
1135
|
+
global.fetch = vi.fn();
|
|
1136
|
+
|
|
1137
|
+
(fetch as any).mockResolvedValueOnce({
|
|
1138
|
+
ok: true,
|
|
1139
|
+
json: async () => ({
|
|
1140
|
+
id: "1",
|
|
1141
|
+
name: "Taha",
|
|
1142
|
+
}),
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const user = await api.user.getUser({
|
|
1146
|
+
path: {
|
|
1147
|
+
id: "1",
|
|
1148
|
+
},
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
expect(user.name).toBe("Taha");
|
|
1152
|
+
```
|
|
287
1153
|
|
|
288
|
-
|
|
1154
|
+
Recommended test coverage:
|
|
1155
|
+
|
|
1156
|
+
* Request validation
|
|
1157
|
+
* Response validation
|
|
1158
|
+
* Path parameter handling
|
|
1159
|
+
* Query string generation
|
|
1160
|
+
* JSON body serialization
|
|
1161
|
+
* Header merging
|
|
1162
|
+
* Auth token injection
|
|
1163
|
+
* Token provider behavior
|
|
1164
|
+
* Middleware execution order
|
|
1165
|
+
* Retry behavior
|
|
1166
|
+
* Timeout and abort behavior
|
|
1167
|
+
* Mock mode
|
|
1168
|
+
* Response wrappers
|
|
1169
|
+
* Error normalization
|
|
1170
|
+
* Encryption middleware
|
|
289
1171
|
|
|
290
1172
|
---
|
|
291
1173
|
|
|
292
|
-
|
|
1174
|
+
## Notes
|
|
293
1175
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1176
|
+
* Always call `client.init()` before using `client.modules`.
|
|
1177
|
+
* All request inputs are validated with Zod.
|
|
1178
|
+
* All successful responses are validated with Zod.
|
|
1179
|
+
* Structured request schemas are recommended for new APIs.
|
|
1180
|
+
* Flat request schemas are still supported for backward compatibility.
|
|
1181
|
+
* `GET` requests do not send a body.
|
|
1182
|
+
* `form-data` endpoints should use `bodyType: "form-data"`.
|
|
1183
|
+
* Auth tokens are only required for endpoints with `auth: true`.
|
|
1184
|
+
* Mock data bypasses network calls but still validates responses.
|
|
298
1185
|
|
|
299
1186
|
---
|
|
300
1187
|
|
|
301
|
-
|
|
1188
|
+
## License
|
|
302
1189
|
|
|
303
1190
|
MIT
|