1000fetches 0.2.2 → 0.3.0
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 +122 -89
- package/dist/client/index.d.ts +23 -0
- package/dist/contract/index.d.ts +6 -0
- package/dist/core.d.ts +36 -10
- package/dist/{errors.d.ts → errors/errors.d.ts} +42 -7
- package/dist/errors/handling.d.ts +2 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -12
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/request/abort/abort.d.ts +12 -0
- package/dist/request/abort/index.d.ts +1 -0
- package/dist/request/fetchErrors/fetchErrors.d.ts +4 -0
- package/dist/request/fetchErrors/index.d.ts +1 -0
- package/dist/request/middleware/index.d.ts +1 -0
- package/dist/request/middleware/middleware.d.ts +7 -0
- package/dist/request/request.d.ts +16 -0
- package/dist/request/retry/index.d.ts +1 -0
- package/dist/request/retry/retry.d.ts +20 -0
- package/dist/request/serialization/index.d.ts +1 -0
- package/dist/request/serialization/serialization.d.ts +9 -0
- package/dist/request/url.d.ts +3 -0
- package/dist/response.d.ts +12 -0
- package/dist/retry-BWvlSmME.cjs +2 -0
- package/dist/retry-BWvlSmME.cjs.map +1 -0
- package/dist/retry-D07aIs-1.js +2 -0
- package/dist/retry-D07aIs-1.js.map +1 -0
- package/dist/{schema.d.ts → schema/index.d.ts} +1 -1
- package/dist/status.d.ts +2 -0
- package/dist/types.d.ts +26 -21
- package/dist/utils/index.d.ts +1 -2
- package/dist/utils/path.d.ts +6 -8
- package/docs/BEST_PRACTICES.md +124 -45
- package/package.json +30 -21
- package/skills/1000fetches/SKILL.md +35 -0
- package/skills/1000fetches/references/client-setup-and-defaults.md +49 -0
- package/skills/1000fetches/references/contracts-and-errors.md +50 -0
- package/skills/1000fetches/references/path-params-and-urls.md +39 -0
- package/skills/1000fetches/references/retries-timeouts-and-middleware.md +32 -0
- package/skills/1000fetches/references/serialization-and-body-types.md +22 -0
- package/dist/client-C7dvgNnD.js +0 -2
- package/dist/client-C7dvgNnD.js.map +0 -1
- package/dist/client-DOv6m2TJ.mjs +0 -2
- package/dist/client-DOv6m2TJ.mjs.map +0 -1
- package/dist/client.d.ts +0 -22
- package/dist/http.cjs +0 -2
- package/dist/http.cjs.map +0 -1
- package/dist/http.d.ts +0 -9
- package/dist/http.mjs +0 -2
- package/dist/http.mjs.map +0 -1
- package/dist/utils/streaming.d.ts +0 -9
- package/docs/API.md +0 -754
- package/docs/MIGRATION.md +0 -719
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ or heavy Axios-style clients that add weight without type guarantees.
|
|
|
22
22
|
|
|
23
23
|
### The idea
|
|
24
24
|
|
|
25
|
-
A type-first HTTP client that unifies validation,
|
|
25
|
+
A type-first HTTP client that unifies validation, dynamic defaults, retries, and middleware on top of native `fetch`.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -30,9 +30,9 @@ A type-first HTTP client that unifies validation, retries, streaming, and middle
|
|
|
30
30
|
|
|
31
31
|
- 🧭 **Compile-time path safety** — `:pathParam` can't slip through undefined
|
|
32
32
|
- 🧩 **Schema-driven validation** — infer types at build time, verify data at runtime
|
|
33
|
-
-
|
|
33
|
+
- ⚙️ **Dynamic defaults** — resolve auth, params, timeouts, and fetch options per request
|
|
34
34
|
- 🔁 **Retries, timeouts, middleware** — production essentials with opt-ins
|
|
35
|
-
- 🎯 **Method-based API** — `api.get()` with
|
|
35
|
+
- 🎯 **Method-based API** — `api.get()` with response contracts `.contract()` and extractor `.data()` for clean and concise code
|
|
36
36
|
- 🧠 **Designed for flow** — clear API, predictable behavior, no hidden magic
|
|
37
37
|
|
|
38
38
|
---
|
|
@@ -59,12 +59,12 @@ const userSchema = z.object({
|
|
|
59
59
|
email: z.email(),
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
// Request data with
|
|
62
|
+
// Request data with a response contract
|
|
63
63
|
const userResponse = await api
|
|
64
64
|
.get('/users/:id', {
|
|
65
65
|
pathParams: { id: '123' },
|
|
66
66
|
})
|
|
67
|
-
.
|
|
67
|
+
.contract(userSchema)
|
|
68
68
|
// userResponse.data 👉 { id: number, ... }
|
|
69
69
|
|
|
70
70
|
// Clean data extraction with full type safety
|
|
@@ -72,28 +72,13 @@ const user = await api
|
|
|
72
72
|
.get('/users/:id', {
|
|
73
73
|
pathParams: { id: '123' },
|
|
74
74
|
})
|
|
75
|
-
.
|
|
75
|
+
.contract(userSchema)
|
|
76
76
|
.data()
|
|
77
77
|
// user 👉 { id: number, ... }
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
**Or use the shortcut client for quick requests with absolute URLs:**
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
import http from '1000fetches/http'
|
|
84
|
-
|
|
85
|
-
const user = await http
|
|
86
|
-
.get('https://api.example.com/users/:id', {
|
|
87
|
-
pathParams: { id: '123' },
|
|
88
|
-
})
|
|
89
|
-
.schema(userSchema)
|
|
90
|
-
.data()
|
|
91
|
-
```
|
|
92
|
-
|
|
93
80
|
With native Node `fetch`, requests must resolve to absolute URLs. Use an absolute `baseUrl` in Node, or provide a custom `fetch` implementation that supports relative URLs.
|
|
94
81
|
|
|
95
|
-
The `1000fetches/http` subpath intentionally exports only this default client. Use `1000fetches` when you need named exports like `createHttpClient` or error classes.
|
|
96
|
-
|
|
97
82
|
## ➡️ Key Features
|
|
98
83
|
|
|
99
84
|
### ✔️ Type Safety That Actually Works
|
|
@@ -109,16 +94,16 @@ The `1000fetches/http` subpath intentionally exports only this default client. U
|
|
|
109
94
|
- **Smart retry logic** — Opt-in exponential backoff for resilient requests
|
|
110
95
|
- **Timeout and cancellation support** — Built-in `AbortController` integration
|
|
111
96
|
- **Structured error handling** — `HttpError`, `NetworkError`, `TimeoutError` with full context
|
|
112
|
-
- **
|
|
97
|
+
- **Dynamic defaults and middleware** — Authentication, logging, transformations
|
|
113
98
|
- **Minimalistic footprint** — Production features without the bloat
|
|
114
99
|
|
|
115
100
|
### ✔️ Engineer-Friendly DX
|
|
116
101
|
|
|
117
102
|
- **Method-based API** — `api.get()`, `api.post()`, `api.put()`, `api.patch()`, `api.delete()`, and generic `api.request()` with full type safety
|
|
118
|
-
- **
|
|
103
|
+
- **Contract-first validation** — Chain `.contract()` for runtime validation and automatic type inference
|
|
119
104
|
- **Smart data extraction** — Chain `.data()` for direct value access without `.data` property
|
|
120
105
|
- **Automatic response parsing** — JSON/text responses parsed automatically
|
|
121
|
-
- **
|
|
106
|
+
- **Per-request defaults** — Resolve auth, params, timeouts, retry settings, and fetch options from request context
|
|
122
107
|
- **Tiny runtime footprint** — one tiny Standard Schema type dependency, tree-shakable builds
|
|
123
108
|
- **TypeScript-first** — Full type inference and `IntelliSense` support
|
|
124
109
|
|
|
@@ -137,43 +122,52 @@ const api = createHttpClient({
|
|
|
137
122
|
// Or dynamic auth
|
|
138
123
|
const api = createHttpClient({
|
|
139
124
|
baseUrl: 'https://api.example.com',
|
|
140
|
-
|
|
125
|
+
defaults: async () => {
|
|
141
126
|
const token = await getToken()
|
|
142
|
-
|
|
143
|
-
|
|
127
|
+
return {
|
|
128
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
129
|
+
}
|
|
144
130
|
},
|
|
145
131
|
})
|
|
146
132
|
```
|
|
147
133
|
|
|
148
|
-
###
|
|
134
|
+
### Dynamic Defaults
|
|
149
135
|
|
|
150
136
|
```ts
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
137
|
+
const api = createHttpClient({
|
|
138
|
+
baseUrl: 'https://api.example.com',
|
|
139
|
+
defaults: ({ resolvedPath, method }) => ({
|
|
140
|
+
headers: { 'X-Client': 'dashboard' },
|
|
141
|
+
timeout: resolvedPath.startsWith('/reports') ? 60_000 : 10_000,
|
|
142
|
+
retry: method === 'GET',
|
|
143
|
+
params: { locale: 'en-US' },
|
|
144
|
+
}),
|
|
159
145
|
})
|
|
146
|
+
```
|
|
160
147
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
148
|
+
### Middleware
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const api = createHttpClient({
|
|
152
|
+
baseUrl: 'https://api.example.com',
|
|
153
|
+
onRequestMiddleware: context => {
|
|
154
|
+
console.log(`→ ${context.method} ${context.url}`)
|
|
155
|
+
return context
|
|
156
|
+
},
|
|
157
|
+
onResponseMiddleware: response => {
|
|
158
|
+
console.log(`← ${response.status} ${response.method} ${response.url}`)
|
|
159
|
+
return response
|
|
169
160
|
},
|
|
170
161
|
})
|
|
171
162
|
```
|
|
172
163
|
|
|
164
|
+
Middleware can observe requests and responses, but it can also mutate them. Keep middleware side-effect-light; use mutation only when dynamic defaults are not enough.
|
|
165
|
+
|
|
173
166
|
### Error Handling
|
|
174
167
|
|
|
175
168
|
```ts
|
|
176
169
|
import {
|
|
170
|
+
AbortError,
|
|
177
171
|
HttpError,
|
|
178
172
|
NetworkError,
|
|
179
173
|
TimeoutError,
|
|
@@ -187,7 +181,7 @@ try {
|
|
|
187
181
|
.get('/users/:id', {
|
|
188
182
|
pathParams: { id: '123' },
|
|
189
183
|
})
|
|
190
|
-
.
|
|
184
|
+
.contract(userSchema)
|
|
191
185
|
} catch (error) {
|
|
192
186
|
if (error instanceof HttpError) {
|
|
193
187
|
console.log(`HTTP ${error.status}: ${error.statusText}`)
|
|
@@ -195,6 +189,8 @@ try {
|
|
|
195
189
|
console.log('Network error:', error.message)
|
|
196
190
|
} else if (error instanceof TimeoutError) {
|
|
197
191
|
console.log('Request timed out')
|
|
192
|
+
} else if (error instanceof AbortError) {
|
|
193
|
+
console.log('Request was cancelled')
|
|
198
194
|
} else if (error instanceof MiddlewareError) {
|
|
199
195
|
console.log('Middleware error:', error.message)
|
|
200
196
|
} else if (error instanceof PathParameterError) {
|
|
@@ -217,19 +213,23 @@ function createHttpClient(config?: HttpClientConfig)
|
|
|
217
213
|
|
|
218
214
|
**Configuration Options:**
|
|
219
215
|
|
|
220
|
-
| Option | Type
|
|
221
|
-
| ---------------------- |
|
|
222
|
-
| `baseUrl` | `string`
|
|
223
|
-
| `headers` | `Record<string, string>`
|
|
224
|
-
| `timeout` | `number`
|
|
225
|
-
| `retry` | `RetryOptions \| boolean`
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
216
|
+
| Option | Type | Description |
|
|
217
|
+
| ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
218
|
+
| `baseUrl` | `string` | Absolute or root-relative base URL |
|
|
219
|
+
| `headers` | `Record<string, string>` | Default headers |
|
|
220
|
+
| `timeout` | `number` | Default timeout in milliseconds |
|
|
221
|
+
| `retry` | `RetryOptions \| boolean` | Opt-in retry configuration |
|
|
222
|
+
| `fetch` | `typeof fetch` | Custom fetch-compatible transport |
|
|
223
|
+
| `serializeBody` | `(body: TBody) => BodyInit \| null \| undefined` | Custom request body serializer |
|
|
224
|
+
| `serializeParams` | `(params) => string` | Custom query parameter serializer |
|
|
225
|
+
| `defaults` | `(context) => RequestDefaults \| void \| Promise<RequestDefaults \| void>` | Dynamic request defaults |
|
|
226
|
+
| `onRequestMiddleware` | `(context: RequestContext) => RequestContext \| void \| Promise<RequestContext \| void>` | Request middleware |
|
|
227
|
+
| `onResponseMiddleware` | `(response: ResponseType) => ResponseType \| Promise<ResponseType>` | Response middleware |
|
|
228
|
+
| `schemaValidator` | `SchemaValidator` | Custom schema validator |
|
|
229
229
|
|
|
230
230
|
### <samp>HTTP Methods</samp>
|
|
231
231
|
|
|
232
|
-
All HTTP methods support
|
|
232
|
+
All HTTP methods support response contracts and data extraction:
|
|
233
233
|
|
|
234
234
|
```ts
|
|
235
235
|
// GET
|
|
@@ -237,12 +237,12 @@ const user = await api
|
|
|
237
237
|
.get('/users/:id', {
|
|
238
238
|
pathParams: { id: '123' },
|
|
239
239
|
})
|
|
240
|
-
.
|
|
240
|
+
.contract(userSchema)
|
|
241
241
|
.data()
|
|
242
242
|
// user 👉 Fully typed user object
|
|
243
243
|
|
|
244
244
|
// POST
|
|
245
|
-
const newUser = await api.post('/users', userData).
|
|
245
|
+
const newUser = await api.post('/users', userData).contract(userSchema).data()
|
|
246
246
|
// newUser 👉 Fully typed user object
|
|
247
247
|
|
|
248
248
|
// PUT
|
|
@@ -250,7 +250,7 @@ const updatedUser = await api
|
|
|
250
250
|
.put('/users/:id', userData, {
|
|
251
251
|
pathParams: { id: '123' },
|
|
252
252
|
})
|
|
253
|
-
.
|
|
253
|
+
.contract(userSchema)
|
|
254
254
|
.data()
|
|
255
255
|
// updatedUser 👉 Fully typed user object
|
|
256
256
|
|
|
@@ -259,7 +259,7 @@ const patchedUser = await api
|
|
|
259
259
|
.patch('/users/:id', partialData, {
|
|
260
260
|
pathParams: { id: '123' },
|
|
261
261
|
})
|
|
262
|
-
.
|
|
262
|
+
.contract(userSchema)
|
|
263
263
|
.data()
|
|
264
264
|
// patchedUser 👉 Fully typed user object
|
|
265
265
|
|
|
@@ -271,27 +271,25 @@ await api.delete('/users/:id', {
|
|
|
271
271
|
|
|
272
272
|
### <samp>Request Options</samp>
|
|
273
273
|
|
|
274
|
-
| Option
|
|
275
|
-
|
|
|
276
|
-
| `pathParams`
|
|
277
|
-
| `params`
|
|
278
|
-
| `headers`
|
|
279
|
-
| `body`
|
|
280
|
-
| `timeout`
|
|
281
|
-
| `signal`
|
|
282
|
-
| `responseType`
|
|
283
|
-
| `cache`
|
|
284
|
-
| `credentials`
|
|
285
|
-
| `mode`
|
|
286
|
-
| `redirect`
|
|
287
|
-
| `fetchOptions`
|
|
288
|
-
| `retry`
|
|
289
|
-
| `onUploadStreaming` | `(event: UploadStreamingEvent) => void` | Upload streaming callback |
|
|
290
|
-
| `onDownloadStreaming` | `(event: DownloadStreamingEvent) => void` | Download streaming callback |
|
|
274
|
+
| Option | Type | Description |
|
|
275
|
+
| -------------- | ----------------------------------------------------------- | --------------------------------- |
|
|
276
|
+
| `pathParams` | `Record<string, string \| number \| undefined>` | Path parameters for URL templates |
|
|
277
|
+
| `params` | `RequestParamsType` | Query parameters |
|
|
278
|
+
| `headers` | `Record<string, string>` | Request headers |
|
|
279
|
+
| `body` | `TBody` | Request body |
|
|
280
|
+
| `timeout` | `number` | Request timeout |
|
|
281
|
+
| `signal` | `AbortSignal` | Request cancellation signal |
|
|
282
|
+
| `responseType` | `'json' \| 'text' \| 'blob' \| 'arrayBuffer' \| 'formData'` | Response type override |
|
|
283
|
+
| `cache` | `RequestCache` | Cache mode |
|
|
284
|
+
| `credentials` | `RequestCredentials` | Credentials mode |
|
|
285
|
+
| `mode` | `RequestMode` | Request mode |
|
|
286
|
+
| `redirect` | `RequestRedirect` | Redirect mode |
|
|
287
|
+
| `fetchOptions` | `FetchOptions` | Native Fetch/Node passthrough |
|
|
288
|
+
| `retry` | `RetryOptions \| boolean` | Retry configuration |
|
|
291
289
|
|
|
292
290
|
### <samp>Retry Options</samp>
|
|
293
291
|
|
|
294
|
-
Retries are disabled unless you set client `retry`, request `retry: true`, or request retry settings. The built-in policy retries idempotent methods by default (`GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`), honors `Retry-After`, supports jitter, and never retries one-shot request
|
|
292
|
+
Retries are disabled unless you set client `retry`, request `retry: true`, or request retry settings. The built-in policy retries idempotent methods by default (`GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`), honors `Retry-After`, supports jitter, and never retries one-shot `ReadableStream` request bodies.
|
|
295
293
|
|
|
296
294
|
```ts
|
|
297
295
|
const api = createHttpClient({
|
|
@@ -306,6 +304,21 @@ const api = createHttpClient({
|
|
|
306
304
|
})
|
|
307
305
|
```
|
|
308
306
|
|
|
307
|
+
### <samp>Status and Empty Responses</samp>
|
|
308
|
+
|
|
309
|
+
By default, 1000fetches treats any 2xx response as successful. Declare valid non-2xx protocol flows, such as `304 Not Modified`, in response contracts.
|
|
310
|
+
|
|
311
|
+
Responses that cannot carry a body (`204`, `205`, `304`, and `HEAD`) resolve with `data: undefined` without attempting JSON parsing.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const response = await api.get('/cached-resource').contract({
|
|
315
|
+
success: {
|
|
316
|
+
default: CachedResourceSchema,
|
|
317
|
+
304: z.undefined(),
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
309
322
|
### <samp>Response Object</samp>
|
|
310
323
|
|
|
311
324
|
All requests return a `ResponseType<T>` object:
|
|
@@ -322,32 +335,52 @@ interface ResponseType<T> {
|
|
|
322
335
|
}
|
|
323
336
|
```
|
|
324
337
|
|
|
325
|
-
### <samp>
|
|
338
|
+
### <samp>Response Contracts</samp>
|
|
326
339
|
|
|
327
|
-
The library provides a chainable API for
|
|
340
|
+
The library provides a chainable API for response contracts:
|
|
328
341
|
|
|
329
342
|
```ts
|
|
330
|
-
// Without
|
|
343
|
+
// Without contract
|
|
331
344
|
const response = await api.get('/users/:id', {
|
|
332
345
|
pathParams: { id: '123' },
|
|
333
346
|
})
|
|
334
347
|
// response 👉 ResponseType<unknown>
|
|
335
348
|
|
|
336
|
-
// With schema
|
|
349
|
+
// With a schema contract
|
|
337
350
|
const response = await api
|
|
338
351
|
.get('/users/:id', {
|
|
339
352
|
pathParams: { id: '123' },
|
|
340
353
|
})
|
|
341
|
-
.
|
|
354
|
+
.contract(userSchema)
|
|
342
355
|
// response 👉 ResponseType<User>
|
|
343
356
|
```
|
|
344
357
|
|
|
358
|
+
Contracts can also describe success and error bodies by status:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
await api.post('/transactions', transactionInput).contract({
|
|
362
|
+
success: {
|
|
363
|
+
default: CreatedTransactionSchema,
|
|
364
|
+
201: EmptyResponseSchema,
|
|
365
|
+
},
|
|
366
|
+
error: {
|
|
367
|
+
default: ErrorSchema,
|
|
368
|
+
400: BadTransactionResponseSchema,
|
|
369
|
+
409: AlreadyCreatedTransactionResponseSchema,
|
|
370
|
+
},
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Success status maps accept only `2xx` and `3xx` status keys. Error status maps accept only `4xx` and `5xx` status keys. Success map status keys control which non-2xx responses resolve; error contracts validate failed responses before `HttpError` is thrown.
|
|
375
|
+
|
|
376
|
+
Error branches validate failed HTTP responses before throwing `HttpError` with the validated error data. If an error response fails its own contract, 1000fetches throws `ContractValidationError` with the status, method, URL, response, schema, and original data.
|
|
377
|
+
|
|
345
378
|
### <samp>Data Extraction</samp>
|
|
346
379
|
|
|
347
380
|
The method-based API provides clean data extraction with full type safety:
|
|
348
381
|
|
|
349
382
|
```ts
|
|
350
|
-
// Extract untyped data (without
|
|
383
|
+
// Extract untyped data (without contract)
|
|
351
384
|
const data = await api
|
|
352
385
|
.get('/users/:id', {
|
|
353
386
|
pathParams: { id: '123' },
|
|
@@ -355,22 +388,22 @@ const data = await api
|
|
|
355
388
|
.data()
|
|
356
389
|
// data 👉 unknown
|
|
357
390
|
|
|
358
|
-
// Direct typed data extraction (with
|
|
391
|
+
// Direct typed data extraction (with contract)
|
|
359
392
|
const user = await api
|
|
360
393
|
.get('/users/:id', {
|
|
361
394
|
pathParams: { id: '123' },
|
|
362
395
|
})
|
|
363
|
-
.
|
|
396
|
+
.contract(userSchema)
|
|
364
397
|
.data()
|
|
365
398
|
// user 👉 { id: number, ... }
|
|
366
399
|
|
|
367
400
|
// Works with all HTTP methods
|
|
368
|
-
const newUser = await api.post('/users', userData).
|
|
401
|
+
const newUser = await api.post('/users', userData).contract(userSchema).data()
|
|
369
402
|
// newUser 👉 Fully typed user object
|
|
370
403
|
|
|
371
404
|
const updatedUser = await api
|
|
372
405
|
.put('/users/:id', userData, { pathParams: { id: '123' } })
|
|
373
|
-
.
|
|
406
|
+
.contract(userSchema)
|
|
374
407
|
.data()
|
|
375
408
|
// updatedUser 👉 Fully typed user object
|
|
376
409
|
|
|
@@ -389,9 +422,9 @@ await api.delete('/users/:id', { pathParams: { id: '123' } }).void()
|
|
|
389
422
|
| **Schema Validation** | Standard Schema support for Zod, Valibot, ArkType, and more |
|
|
390
423
|
| **Retry Logic** | Opt-in retry policy with backoff, jitter, and `Retry-After` |
|
|
391
424
|
| **Error Handling** | Structured errors for HTTP, network, timeout, and validation failures |
|
|
425
|
+
| **Dynamic Defaults** | Per-request headers, params, timeout, retry, and fetch options |
|
|
392
426
|
| **Middleware** | Request and response middleware with typed context |
|
|
393
|
-
| **
|
|
394
|
-
| **Tree Shaking** | Side-effect-free ESM/CJS builds with a default-only `./http` shortcut |
|
|
427
|
+
| **Tree Shaking** | Side-effect-free ESM/CJS builds |
|
|
395
428
|
|
|
396
429
|
---
|
|
397
430
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { HttpClientConfig, HttpRequestOptions } from '../core';
|
|
2
|
+
import { ContractableResponse, EnforcedPathParamsOptions, RequestParamsType } from '../types';
|
|
3
|
+
import { AssertSupportedPath, HasRequiredParams, RequirePathParams } from '../utils';
|
|
4
|
+
type PathOptionsArgs<Path extends string, TOptions> = [
|
|
5
|
+
AssertSupportedPath<Path>
|
|
6
|
+
] extends [never] ? [options: never] : HasRequiredParams<AssertSupportedPath<Path>> extends true ? [options: TOptions] : [options?: TOptions];
|
|
7
|
+
type EnforcedHttpRequestOptions<TBody, TParams extends RequestParamsType, Path extends string> = RequirePathParams<AssertSupportedPath<Path>, string extends Path ? HttpRequestOptions<TBody, TParams> : Omit<HttpRequestOptions<TBody, TParams>, 'pathParams'>>;
|
|
8
|
+
/**
|
|
9
|
+
* Creates a complete HTTP client with method builders
|
|
10
|
+
*/
|
|
11
|
+
export declare function createHttpClient<TSerializedBody = unknown>(config?: HttpClientConfig<TSerializedBody>): {
|
|
12
|
+
get: <Path extends string = string, TParams extends RequestParamsType = RequestParamsType>(url: Path, ...args: PathOptionsArgs<Path, EnforcedPathParamsOptions<never, TParams, Path>>) => ContractableResponse<unknown>;
|
|
13
|
+
post: <Path extends string = string, TBody extends TSerializedBody = TSerializedBody, TParams extends RequestParamsType = RequestParamsType>(url: Path, body?: TBody, ...args: PathOptionsArgs<Path, EnforcedPathParamsOptions<TBody, TParams, Path>>) => ContractableResponse<unknown>;
|
|
14
|
+
put: <Path extends string = string, TBody extends TSerializedBody = TSerializedBody, TParams extends RequestParamsType = RequestParamsType>(url: Path, body?: TBody, ...args: PathOptionsArgs<Path, EnforcedPathParamsOptions<TBody, TParams, Path>>) => ContractableResponse<unknown>;
|
|
15
|
+
patch: <Path extends string = string, TBody extends TSerializedBody = TSerializedBody, TParams extends RequestParamsType = RequestParamsType>(url: Path, body?: TBody, ...args: PathOptionsArgs<Path, EnforcedPathParamsOptions<TBody, TParams, Path>>) => ContractableResponse<unknown>;
|
|
16
|
+
delete: <Path extends string = string, TParams extends RequestParamsType = RequestParamsType>(url: Path, ...args: PathOptionsArgs<Path, EnforcedPathParamsOptions<never, TParams, Path>>) => ContractableResponse<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Generic request method for custom HTTP methods and full control
|
|
19
|
+
*/
|
|
20
|
+
request: <Path extends string = string, TBody extends TSerializedBody = TSerializedBody, TParams extends RequestParamsType = RequestParamsType>(url: Path, ...args: PathOptionsArgs<Path, EnforcedHttpRequestOptions<TBody, TParams, Path>>) => ContractableResponse<unknown>;
|
|
21
|
+
};
|
|
22
|
+
export type HttpClient = ReturnType<typeof createHttpClient>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { HttpRequestOptions } from '../core';
|
|
2
|
+
import { SchemaValidator } from '../schema';
|
|
3
|
+
import { InferContractSuccess, RequestParamsType, ResponseContract, ResponseType } from '../types';
|
|
4
|
+
export declare function validateResponseContractShape(contract: ResponseContract): void;
|
|
5
|
+
export declare function createContractRequestOptions<TBody, TParams extends RequestParamsType>(options: HttpRequestOptions<TBody, TParams>, contract: ResponseContract): HttpRequestOptions<TBody, TParams>;
|
|
6
|
+
export declare function validateResponsePromiseWithContract<C extends ResponseContract>(contract: C, responsePromise: Promise<ResponseType<unknown>>, schemaValidator: SchemaValidator): Promise<ResponseType<InferContractSuccess<C>>>;
|
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaValidator } from './schema';
|
|
2
|
-
import { CustomFetch,
|
|
2
|
+
import { CustomFetch, FetchOptions, HttpHeaders, HttpMethod, RequestContext, RequestParamsType, ResponseType, RetryOptions, SerializeBody, SerializeParams } from './types';
|
|
3
3
|
export interface HttpRequestOptions<TBody = unknown, TParams extends RequestParamsType = RequestParamsType> {
|
|
4
4
|
method?: HttpMethod;
|
|
5
5
|
headers?: HttpHeaders;
|
|
@@ -18,16 +18,40 @@ export interface HttpRequestOptions<TBody = unknown, TParams extends RequestPara
|
|
|
18
18
|
redirect?: RequestRedirect;
|
|
19
19
|
/** Additional Fetch/Node options passed through to the underlying fetch call */
|
|
20
20
|
fetchOptions?: FetchOptions;
|
|
21
|
-
/** Upload streaming tracking for this specific request */
|
|
22
|
-
onUploadStreaming?: (event: UploadStreamingEvent) => void;
|
|
23
|
-
/** Download streaming tracking for this specific request */
|
|
24
|
-
onDownloadStreaming?: (event: DownloadStreamingEvent) => void;
|
|
25
21
|
/** Response type override */
|
|
26
22
|
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData';
|
|
27
23
|
/** Per-request retry configuration */
|
|
28
24
|
retry?: RetryOptions | boolean;
|
|
29
25
|
}
|
|
30
|
-
export interface
|
|
26
|
+
export interface RequestDefaults {
|
|
27
|
+
headers?: HttpHeaders;
|
|
28
|
+
params?: RequestParamsType;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
credentials?: RequestCredentials;
|
|
31
|
+
cache?: RequestCache;
|
|
32
|
+
mode?: RequestMode;
|
|
33
|
+
redirect?: RequestRedirect;
|
|
34
|
+
fetchOptions?: FetchOptions;
|
|
35
|
+
responseType?: HttpRequestOptions['responseType'];
|
|
36
|
+
retry?: RetryOptions | boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface RequestDefaultsContext<TBody = unknown, TParams extends RequestParamsType = RequestParamsType, Path extends string = string> {
|
|
39
|
+
/** Raw path or URL passed by the caller before path parameter generation */
|
|
40
|
+
path: Path;
|
|
41
|
+
/** Path after template parameter generation, before baseUrl and query params */
|
|
42
|
+
resolvedPath: string;
|
|
43
|
+
method: HttpMethod;
|
|
44
|
+
params?: TParams;
|
|
45
|
+
pathParams?: Record<string, string | number | undefined>;
|
|
46
|
+
body?: TBody;
|
|
47
|
+
hasBody: boolean;
|
|
48
|
+
requestOptions: Readonly<HttpRequestOptions<TBody, TParams>>;
|
|
49
|
+
baseUrl: string;
|
|
50
|
+
headers: HttpHeaders;
|
|
51
|
+
timeout: number;
|
|
52
|
+
retry?: RetryOptions | boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface HttpClientConfig<TSerializedBody = unknown> {
|
|
31
55
|
/** Base URL for all requests */
|
|
32
56
|
baseUrl?: string;
|
|
33
57
|
/** Default headers */
|
|
@@ -41,17 +65,19 @@ export interface HttpClientConfig {
|
|
|
41
65
|
/** Custom fetch implementation */
|
|
42
66
|
fetch?: CustomFetch;
|
|
43
67
|
/** Custom body serializer */
|
|
44
|
-
serializeBody?: SerializeBody
|
|
68
|
+
serializeBody?: SerializeBody<TSerializedBody>;
|
|
45
69
|
/** Custom params serializer */
|
|
46
70
|
serializeParams?: SerializeParams;
|
|
71
|
+
/** Per-request defaults resolved before request options and middleware */
|
|
72
|
+
defaults?: <TBody extends TSerializedBody = TSerializedBody, TParams extends RequestParamsType = RequestParamsType, Path extends string = string>(context: RequestDefaultsContext<TBody, TParams, Path>) => RequestDefaults | void | Promise<RequestDefaults | void>;
|
|
47
73
|
/** Request middleware - can modify request before sending */
|
|
48
|
-
onRequestMiddleware?: <TBody = unknown>(context: RequestContext<TBody>) => RequestContext<TBody> | Promise<RequestContext<TBody
|
|
74
|
+
onRequestMiddleware?: <TBody = unknown>(context: RequestContext<TBody>) => RequestContext<TBody> | void | Promise<RequestContext<TBody> | void>;
|
|
49
75
|
/** Response middleware - can modify response after receiving */
|
|
50
76
|
onResponseMiddleware?: (response: ResponseType<unknown>) => ResponseType<unknown> | Promise<ResponseType<unknown>>;
|
|
51
77
|
}
|
|
52
78
|
/**
|
|
53
79
|
* Creates an HTTP request handler with the given configuration.
|
|
54
80
|
* This is the core function that handles all HTTP requests with retry logic,
|
|
55
|
-
*
|
|
81
|
+
* middleware, serialization, defaults, and timeouts.
|
|
56
82
|
*/
|
|
57
|
-
export declare function createHttpRequest(config?: HttpClientConfig): <Path extends string = string, TResponse = unknown, TBody =
|
|
83
|
+
export declare function createHttpRequest<TSerializedBody = unknown>(config?: HttpClientConfig<TSerializedBody>): <Path extends string = string, TResponse = unknown, TBody extends TSerializedBody = TSerializedBody, TParams extends RequestParamsType = RequestParamsType>(url: Path, options?: HttpRequestOptions<TBody, TParams>) => Promise<ResponseType<TResponse>>;
|
|
@@ -22,18 +22,53 @@ export declare class NetworkError extends Error implements HttpClientError {
|
|
|
22
22
|
constructor(message: string, cause?: Error);
|
|
23
23
|
}
|
|
24
24
|
export declare class SchemaValidationError extends Error implements HttpClientError {
|
|
25
|
-
readonly name
|
|
25
|
+
readonly name: string;
|
|
26
26
|
readonly schema: unknown;
|
|
27
27
|
readonly data: unknown;
|
|
28
28
|
readonly issues?: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
29
29
|
readonly cause?: Error;
|
|
30
30
|
constructor(message: string, schema: unknown, data: unknown, cause?: Error, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
31
31
|
}
|
|
32
|
+
export declare class ContractValidationError extends SchemaValidationError {
|
|
33
|
+
readonly name = "ContractValidationError";
|
|
34
|
+
readonly branch: 'success' | 'error';
|
|
35
|
+
readonly status: number;
|
|
36
|
+
readonly statusText: string;
|
|
37
|
+
readonly response: Response;
|
|
38
|
+
readonly url: string;
|
|
39
|
+
readonly method: string;
|
|
40
|
+
constructor(message: string, schema: unknown, data: unknown, response: {
|
|
41
|
+
status: number;
|
|
42
|
+
statusText: string;
|
|
43
|
+
raw: Response;
|
|
44
|
+
url: string;
|
|
45
|
+
method: string;
|
|
46
|
+
}, branch: 'success' | 'error', cause?: Error, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
47
|
+
}
|
|
48
|
+
export declare class InvalidContractError extends Error implements HttpClientError {
|
|
49
|
+
readonly name = "InvalidContractError";
|
|
50
|
+
readonly contract: unknown;
|
|
51
|
+
readonly status?: number;
|
|
52
|
+
readonly branch?: 'success' | 'error';
|
|
53
|
+
readonly cause?: Error;
|
|
54
|
+
constructor(message: string, contract: unknown, status?: number, branch?: 'success' | 'error', cause?: Error);
|
|
55
|
+
}
|
|
32
56
|
export declare class TimeoutError extends Error implements HttpClientError {
|
|
33
57
|
readonly name = "TimeoutError";
|
|
34
58
|
readonly cause?: Error;
|
|
35
59
|
constructor(message: string, cause?: Error);
|
|
36
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Thrown when a request is cancelled by a caller-provided AbortSignal.
|
|
63
|
+
*
|
|
64
|
+
* Timeouts are reported as TimeoutError; this class represents explicit
|
|
65
|
+
* cancellation only.
|
|
66
|
+
*/
|
|
67
|
+
export declare class AbortError extends Error implements HttpClientError {
|
|
68
|
+
readonly name = "AbortError";
|
|
69
|
+
readonly cause?: Error;
|
|
70
|
+
constructor(message?: string, cause?: Error);
|
|
71
|
+
}
|
|
37
72
|
export declare class PathParameterError extends Error implements HttpClientError {
|
|
38
73
|
readonly name = "PathParameterError";
|
|
39
74
|
readonly url: string;
|
|
@@ -55,17 +90,17 @@ export declare class SerializationError extends Error implements HttpClientError
|
|
|
55
90
|
readonly cause?: Error;
|
|
56
91
|
constructor(message: string, cause?: Error);
|
|
57
92
|
}
|
|
58
|
-
|
|
59
|
-
readonly name
|
|
93
|
+
declare abstract class SchemaDefinitionError extends Error implements HttpClientError {
|
|
94
|
+
abstract readonly name: string;
|
|
60
95
|
readonly schema: unknown;
|
|
61
96
|
readonly cause?: Error;
|
|
62
97
|
constructor(message: string, schema: unknown, cause?: Error);
|
|
63
98
|
}
|
|
64
|
-
export declare class
|
|
99
|
+
export declare class InvalidSchemaError extends SchemaDefinitionError {
|
|
100
|
+
readonly name = "InvalidSchemaError";
|
|
101
|
+
}
|
|
102
|
+
export declare class AsyncSchemaValidationError extends SchemaDefinitionError {
|
|
65
103
|
readonly name = "AsyncSchemaValidationError";
|
|
66
|
-
readonly schema: unknown;
|
|
67
|
-
readonly cause?: Error;
|
|
68
|
-
constructor(message: string, schema: unknown, cause?: Error);
|
|
69
104
|
}
|
|
70
105
|
export declare class InvalidBaseUrlError extends Error implements HttpClientError {
|
|
71
106
|
readonly name = "InvalidBaseUrlError";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AbortError, AsyncSchemaValidationError, ContractValidationError, HttpError, InvalidBaseUrlError, InvalidContractError, InvalidSchemaError, MiddlewareError, NetworkError, PathParameterError, SchemaValidationError, SerializationError, TimeoutError, } from './errors';
|