@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 CHANGED
@@ -1,18 +1,24 @@
1
1
  # TypeFetch
2
2
 
3
- TypeFetch is a strongly-typed HTTP client built on **TypeScript** and **Zod**.
3
+ TypeFetch is a production-grade, strongly-typed HTTP client built on
4
+ **TypeScript** and **Zod**.
4
5
 
5
- You define your API once using Zod schemas, and TypeFetch generates a fully type-safe client with:
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
- ## Core Concepts
35
+ # What's New / Updated
30
36
 
31
- ### 1. Type-Safe API Client
37
+ ## 1. Advanced Retry Engine
32
38
 
33
- Define your API with Zod schemas and get full type safety for request and response types.
39
+ TypeFetch now includes:
34
40
 
35
- Each endpoint has:
41
+ - Configurable `maxRetries`
42
+ - Custom `retryCondition`
43
+ - Built-in backoff strategies:
44
+ - `fixed`
45
+ - `exponential`
46
+ - Fully normalized retry errors
36
47
 
37
- - `method`: HTTP verb (`GET | POST | PUT | PATCH | DELETE`)
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
- ### 2. Structured Request Shape
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
- The recommended request shape is:
81
+ ---
82
+
83
+ ## 4. Structured Request Model (Canonical Format)
49
84
 
50
85
  ```ts
51
86
  z.object({
52
- path: z.object({ ... }).optional(), // URL params → /users/:id
53
- query: z.object({ ... }).optional(), // query string → ?page=1
54
- body: z.object({ ... }).optional(), // JSON body
55
- headers: z.record(z.string()).optional(), // per-call extra headers
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 will:
94
+ TypeFetch automatically:
60
95
 
61
- - Replace `:param` segments in the path using `path`
62
- - Build query string from `query`
63
- - Serialize `body` as JSON (or `FormData` if `bodyType: "form-data"`)
64
- - Merge headers from:
65
- - auth (Authorization)
66
- - endpoint-level `headers`
67
- - per-call `headers` in the request (highest priority)
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
- ### 3. Backward Compatibility
104
+ ---
105
+
106
+ ## 5. Backward Compatibility
107
+
108
+ Flat request schemas still work:
70
109
 
71
- If your `request` schema is **flat** (e.g. `z.object({ name: z.string() })`) and does **not** contain `path`, `query`, `body`, or `headers`, TypeFetch treats the entire object as the request body for non-GET methods.
110
+ ```ts
111
+ z.object({
112
+ name: z.string(),
113
+ });
114
+ ```
72
115
 
73
- This makes migration to the structured format incremental and safe.
116
+ For non-GET requests, the entire object becomes the JSON body.
74
117
 
75
118
  ---
76
119
 
77
- ## Defining API Contracts
120
+ # Defining API Contracts
78
121
 
79
- Example contract definition:
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() }).optional(),
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
- ## Using `ApiClient`
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", // or undefined for public endpoints
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
- ## Middlewares
169
+ # Middleware System
164
170
 
165
- Middlewares allow you to hook into the request/response lifecycle.
171
+ Middlewares execute in reverse registration order.
166
172
 
167
- ### Custom Middleware Example
173
+ ## Custom Middleware
168
174
 
169
175
  ```ts
170
176
  client.use(async (ctx, next) => {
171
- console.log("Request to:", ctx.url);
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
- ### Built-in Middlewares
184
+ ## Built-in Middlewares
179
185
 
180
- ```ts
181
- import {
182
- loggingMiddleware,
183
- retryMiddleware,
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
- - **`loggingMiddleware`** – logs requests and responses (controlled by `debug`, `logRequest`, `logResponse`).
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.setMockMode(true, { min: 200, max: 1000 }); // simulate network delay
213
- // ...
214
- client.setMockMode(false);
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
- ## Response Transformation
222
-
223
- You can apply a global transformation to all successful responses:
202
+ # Mock Mode
224
203
 
225
204
  ```ts
226
- client.useResponseTransform((data) => {
227
- return {
228
- ...data,
229
- transformedAt: new Date().toISOString(),
230
- };
231
- });
205
+ client.setMockMode(true, { min: 200, max: 1000 });
232
206
  ```
233
207
 
234
- This runs after:
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
- ## Response Wrapper Example
213
+ # Response Wrapper
243
214
 
244
- For APIs that wrap responses like this:
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
- You can define a single wrapper schema:
225
+ Example:
256
226
 
257
227
  ```ts
258
- import { z } from "zod";
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 `success: true`, `data` is passed to the endpoint’s `response` schema.
281
- On `success: false`, a `RichError` is thrown with normalized information.
231
+ On failure, throws normalized `RichError`.
282
232
 
283
233
  ---
284
234
 
285
- ## Error Handling
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("API Error:", err.message, err.status, err.code);
249
+ console.error(err.message, err.status);
290
250
  });
291
251
  ```
292
252
 
293
- Errors are normalized into `RichError` (or your custom type if you change the generic). Zod validation errors are also wrapped into a `VALIDATION_ERROR` with a readable message.
253
+ ---
294
254
 
295
- You can still handle errors per-call with `try/catch`:
255
+ # File Uploads (FormData)
296
256
 
297
- ```ts
298
- try {
299
- const user = await api.user.getUser({ path: { id: "123" } });
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
- ## File Uploads (`form-data`)
263
+ # Concurrency Safety
308
264
 
309
- For endpoints that need file upload, set `bodyType: "form-data"` and put file(s) inside `body`:
265
+ TypeFetch safely handles parallel requests:
310
266
 
311
- ```ts
312
- const uploadAvatarRequest = z.object({
313
- path: z.object({}).optional(),
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
- const uploadAvatarResponse = z.object({
322
- url: z.string(),
323
- });
271
+ ---
324
272
 
325
- const contracts = {
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
- Usage:
275
+ The project now includes:
340
276
 
341
- ```ts
342
- const file = input.files?.[0];
343
- await api.user.uploadAvatar({
344
- body: { file },
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
- The client will build a `FormData` object and let the browser set the `Content-Type` header.
288
+ Suitable for publishing as a production SDK.
349
289
 
350
290
  ---
351
291
 
352
- ## Notes
292
+ # Notes
353
293
 
354
- - Always call `client.init()` before using `client.modules`.
355
- - Middlewares execute in **reverse registration order** (last registered runs first).
356
- - Endpoints with `auth: true` require a valid token from `token` or `tokenProvider`.
357
- - All responses are parsed and validated by Zod using each endpoint’s `response` schema.
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
- ## License
301
+ # License
363
302
 
364
303
  MIT