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.
Files changed (55) hide show
  1. package/README.md +122 -89
  2. package/dist/client/index.d.ts +23 -0
  3. package/dist/contract/index.d.ts +6 -0
  4. package/dist/core.d.ts +36 -10
  5. package/dist/{errors.d.ts → errors/errors.d.ts} +42 -7
  6. package/dist/errors/handling.d.ts +2 -0
  7. package/dist/errors/index.d.ts +1 -0
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.ts +4 -12
  11. package/dist/index.mjs +1 -1
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/request/abort/abort.d.ts +12 -0
  14. package/dist/request/abort/index.d.ts +1 -0
  15. package/dist/request/fetchErrors/fetchErrors.d.ts +4 -0
  16. package/dist/request/fetchErrors/index.d.ts +1 -0
  17. package/dist/request/middleware/index.d.ts +1 -0
  18. package/dist/request/middleware/middleware.d.ts +7 -0
  19. package/dist/request/request.d.ts +16 -0
  20. package/dist/request/retry/index.d.ts +1 -0
  21. package/dist/request/retry/retry.d.ts +20 -0
  22. package/dist/request/serialization/index.d.ts +1 -0
  23. package/dist/request/serialization/serialization.d.ts +9 -0
  24. package/dist/request/url.d.ts +3 -0
  25. package/dist/response.d.ts +12 -0
  26. package/dist/retry-BWvlSmME.cjs +2 -0
  27. package/dist/retry-BWvlSmME.cjs.map +1 -0
  28. package/dist/retry-D07aIs-1.js +2 -0
  29. package/dist/retry-D07aIs-1.js.map +1 -0
  30. package/dist/{schema.d.ts → schema/index.d.ts} +1 -1
  31. package/dist/status.d.ts +2 -0
  32. package/dist/types.d.ts +26 -21
  33. package/dist/utils/index.d.ts +1 -2
  34. package/dist/utils/path.d.ts +6 -8
  35. package/docs/BEST_PRACTICES.md +124 -45
  36. package/package.json +30 -21
  37. package/skills/1000fetches/SKILL.md +35 -0
  38. package/skills/1000fetches/references/client-setup-and-defaults.md +49 -0
  39. package/skills/1000fetches/references/contracts-and-errors.md +50 -0
  40. package/skills/1000fetches/references/path-params-and-urls.md +39 -0
  41. package/skills/1000fetches/references/retries-timeouts-and-middleware.md +32 -0
  42. package/skills/1000fetches/references/serialization-and-body-types.md +22 -0
  43. package/dist/client-C7dvgNnD.js +0 -2
  44. package/dist/client-C7dvgNnD.js.map +0 -1
  45. package/dist/client-DOv6m2TJ.mjs +0 -2
  46. package/dist/client-DOv6m2TJ.mjs.map +0 -1
  47. package/dist/client.d.ts +0 -22
  48. package/dist/http.cjs +0 -2
  49. package/dist/http.cjs.map +0 -1
  50. package/dist/http.d.ts +0 -9
  51. package/dist/http.mjs +0 -2
  52. package/dist/http.mjs.map +0 -1
  53. package/dist/utils/streaming.d.ts +0 -9
  54. package/docs/API.md +0 -754
  55. 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, retries, streaming, and middleware on top of native `fetch`.
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
- - **Streaming telemetry** — observe upload/download chunks and progress as data flows
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 schema validation `.schema()` and extractor `.data()` for clean and concise code
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 schema validation
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
- .schema(userSchema)
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
- .schema(userSchema)
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
- - **Request/Response middleware** — Authentication, logging, transformations
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
- - **Schema-first validation** — Chain `.schema()` for runtime validation and automatic type inference
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
- - **Chunk telemetry** — Observe upload/download chunks and byte progress
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
- onRequestMiddleware: async context => {
125
+ defaults: async () => {
141
126
  const token = await getToken()
142
- context.headers.set('Authorization', `Bearer ${token}`)
143
- return context
127
+ return {
128
+ headers: { Authorization: `Bearer ${token}` },
129
+ }
144
130
  },
145
131
  })
146
132
  ```
147
133
 
148
- ### Streaming
134
+ ### Dynamic Defaults
149
135
 
150
136
  ```ts
151
- // Upload progress with actual data chunks
152
- await api.post('/api/files', fileData, {
153
- onUploadStreaming: ({ chunk, transferredBytes, totalBytes }) => {
154
- const progress = totalBytes
155
- ? `${Math.round((transferredBytes / totalBytes) * 100)}%`
156
- : `${transferredBytes} bytes`
157
- console.log(`Uploading: ${progress} - chunk size: ${chunk.length} bytes`)
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
- // Download progress
162
- await api.get('/api/files/:id', {
163
- pathParams: { id: '123' },
164
- onDownloadStreaming: ({ chunk, transferredBytes, totalBytes }) => {
165
- const progress = totalBytes
166
- ? `${Math.round((transferredBytes / totalBytes) * 100)}%`
167
- : `${transferredBytes} bytes`
168
- console.log(`Downloading: ${progress} - chunk size: ${chunk.length} bytes`)
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
- .schema(userSchema)
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 | Description |
221
- | ---------------------- | ------------------------------------------------------------------------ | ---------------------------------- |
222
- | `baseUrl` | `string` | Absolute or root-relative base URL |
223
- | `headers` | `Record<string, string>` | Default headers |
224
- | `timeout` | `number` | Default timeout in milliseconds |
225
- | `retry` | `RetryOptions \| boolean` | Opt-in retry configuration |
226
- | `onRequestMiddleware` | `(context: RequestContext) => RequestContext \| Promise<RequestContext>` | Request middleware |
227
- | `onResponseMiddleware` | `(response: ResponseType) => ResponseType \| Promise<ResponseType>` | Response middleware |
228
- | `schemaValidator` | `SchemaValidator` | Custom schema validator |
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 schema validation and data extraction:
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
- .schema(userSchema)
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).schema(userSchema).data()
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
- .schema(userSchema)
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
- .schema(userSchema)
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 | 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 |
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 streams.
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>Schema Validation</samp>
338
+ ### <samp>Response Contracts</samp>
326
339
 
327
- The library provides a chainable API for schema validation:
340
+ The library provides a chainable API for response contracts:
328
341
 
329
342
  ```ts
330
- // Without schema
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
- .schema(userSchema)
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 schema)
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 schema)
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
- .schema(userSchema)
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).schema(userSchema).data()
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
- .schema(userSchema)
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
- | **Streaming** | Upload/download chunk telemetry through callback hooks |
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, DownloadStreamingEvent, FetchOptions, HttpHeaders, HttpMethod, RequestContext, RequestParamsType, ResponseType, RetryOptions, SerializeBody, SerializeParams, UploadStreamingEvent } from './types';
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 HttpClientConfig {
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
- * interceptors, and streaming.
81
+ * middleware, serialization, defaults, and timeouts.
56
82
  */
57
- export declare function createHttpRequest(config?: HttpClientConfig): <Path extends string = string, TResponse = unknown, TBody = unknown, TParams extends RequestParamsType = RequestParamsType>(url: Path, options?: HttpRequestOptions<TBody, TParams>) => Promise<ResponseType<TResponse>>;
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 = "SchemaValidationError";
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
- export declare class InvalidSchemaError extends Error implements HttpClientError {
59
- readonly name = "InvalidSchemaError";
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 AsyncSchemaValidationError extends Error implements HttpClientError {
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,2 @@
1
+ export declare function createErrorMessage(context: string, error: unknown): string;
2
+ export declare function createStandardizedError(error: unknown, context: string): Error;
@@ -0,0 +1 @@
1
+ export { AbortError, AsyncSchemaValidationError, ContractValidationError, HttpError, InvalidBaseUrlError, InvalidContractError, InvalidSchemaError, MiddlewareError, NetworkError, PathParameterError, SchemaValidationError, SerializationError, TimeoutError, } from './errors';