1000fetches 0.2.1 → 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 (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +168 -109
  3. package/dist/client/index.d.ts +23 -0
  4. package/dist/contract/index.d.ts +6 -0
  5. package/dist/core.d.ts +83 -0
  6. package/dist/errors/errors.d.ts +111 -0
  7. package/dist/errors/handling.d.ts +2 -0
  8. package/dist/errors/index.d.ts +1 -0
  9. package/dist/index.cjs +2 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.d.ts +7 -347
  12. package/dist/index.mjs +2 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/request/abort/abort.d.ts +12 -0
  15. package/dist/request/abort/index.d.ts +1 -0
  16. package/dist/request/fetchErrors/fetchErrors.d.ts +4 -0
  17. package/dist/request/fetchErrors/index.d.ts +1 -0
  18. package/dist/request/middleware/index.d.ts +1 -0
  19. package/dist/request/middleware/middleware.d.ts +7 -0
  20. package/dist/request/request.d.ts +16 -0
  21. package/dist/request/retry/index.d.ts +1 -0
  22. package/dist/request/retry/retry.d.ts +20 -0
  23. package/dist/request/serialization/index.d.ts +1 -0
  24. package/dist/request/serialization/serialization.d.ts +9 -0
  25. package/dist/request/url.d.ts +3 -0
  26. package/dist/response.d.ts +12 -0
  27. package/dist/retry-BWvlSmME.cjs +2 -0
  28. package/dist/retry-BWvlSmME.cjs.map +1 -0
  29. package/dist/retry-D07aIs-1.js +2 -0
  30. package/dist/retry-D07aIs-1.js.map +1 -0
  31. package/dist/schema/index.d.ts +35 -0
  32. package/dist/status.d.ts +2 -0
  33. package/dist/types.d.ts +150 -0
  34. package/dist/utils/index.d.ts +1 -0
  35. package/dist/utils/path.d.ts +71 -0
  36. package/docs/BEST_PRACTICES.md +685 -0
  37. package/package.json +52 -56
  38. package/skills/1000fetches/SKILL.md +35 -0
  39. package/skills/1000fetches/references/client-setup-and-defaults.md +49 -0
  40. package/skills/1000fetches/references/contracts-and-errors.md +50 -0
  41. package/skills/1000fetches/references/path-params-and-urls.md +39 -0
  42. package/skills/1000fetches/references/retries-timeouts-and-middleware.md +32 -0
  43. package/skills/1000fetches/references/serialization-and-body-types.md +22 -0
  44. package/dist/index.cjs.js +0 -2
  45. package/dist/index.cjs.js.map +0 -1
  46. package/dist/index.es.js +0 -793
  47. package/dist/index.es.js.map +0 -1
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Max Tarsis
3
+ Copyright (c) 2026 Max Tarsis
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
- - **Native streaming** — observe, transform, or pipe data chunks as they flow
34
- - 🔁 **Retries, timeouts, middleware** — production essentials, zero config
35
- - 🎯 **Method-based API** — `api.get()` with schema validation `.schema()` and extractor `.data()` for clean and concise code
33
+ - ⚙️ **Dynamic defaults** — resolve auth, params, timeouts, and fetch options per request
34
+ - 🔁 **Retries, timeouts, middleware** — production essentials with opt-ins
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
  ---
@@ -55,15 +55,16 @@ const api = createHttpClient({
55
55
  // Define schemas for types and safety
56
56
  const userSchema = z.object({
57
57
  id: z.number(),
58
- ...
58
+ name: z.string(),
59
+ email: z.email(),
59
60
  })
60
61
 
61
- // Request data with schema validation
62
+ // Request data with a response contract
62
63
  const userResponse = await api
63
64
  .get('/users/:id', {
64
65
  pathParams: { id: '123' },
65
66
  })
66
- .schema(userSchema)
67
+ .contract(userSchema)
67
68
  // userResponse.data 👉 { id: number, ... }
68
69
 
69
70
  // Clean data extraction with full type safety
@@ -71,51 +72,42 @@ const user = await api
71
72
  .get('/users/:id', {
72
73
  pathParams: { id: '123' },
73
74
  })
74
- .schema(userSchema)
75
+ .contract(userSchema)
75
76
  .data()
76
77
  // user 👉 { id: number, ... }
77
78
  ```
78
79
 
79
- **Or use the default client for quick requests:**
80
-
81
- ```ts
82
- import http from '1000fetches'
83
-
84
- const user = await http
85
- .get('/users/:id', {
86
- pathParams: { id: '123' },
87
- })
88
- .schema(userSchema)
89
- ```
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.
90
81
 
91
82
  ## ➡️ Key Features
92
83
 
93
84
  ### ✔️ Type Safety That Actually Works
94
85
 
95
86
  - **Compile-time path validation** — TypeScript catches missing `:userId` parameters before runtime
87
+ - **Trailing optional path params only** — `'/users/:id?'` supported, `'/users/:id?/cards/:cardId'` rejected
96
88
  - **Runtime schema validation** — Schemas verify data at the network boundary
97
89
  - **Schema-based type inference** — Types inferred from schemas, no generics needed
98
90
  - **Multi-schema support** — [Zod](https://github.com/colinhacks/zod), [Valibot](https://github.com/fabian-hiller/valibot), [ArkType](https://github.com/arktypeio/arktype), or any [Standard Schema](https://github.com/standard-schema/standard-schema)-compatible library
99
91
 
100
92
  ### ✔️ Production-Ready Quality
101
93
 
102
- - **Smart retry logic** — Exponential backoff with jitter for resilient requests
94
+ - **Smart retry logic** — Opt-in exponential backoff for resilient requests
103
95
  - **Timeout and cancellation support** — Built-in `AbortController` integration
104
96
  - **Structured error handling** — `HttpError`, `NetworkError`, `TimeoutError` with full context
105
- - **Request/Response middleware** — Authentication, logging, transformations
106
- - **Minimalistic footprint** — Enterprise features without the bloat
97
+ - **Dynamic defaults and middleware** — Authentication, logging, transformations
98
+ - **Minimalistic footprint** — Production features without the bloat
107
99
 
108
100
  ### ✔️ Engineer-Friendly DX
109
101
 
110
102
  - **Method-based API** — `api.get()`, `api.post()`, `api.put()`, `api.patch()`, `api.delete()`, and generic `api.request()` with full type safety
111
- - **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
112
104
  - **Smart data extraction** — Chain `.data()` for direct value access without `.data` property
113
105
  - **Automatic response parsing** — JSON/text responses parsed automatically
114
- - **Real-time streaming** — Access actual data chunks during upload/download
115
- - **Zero dependencies** — Optional peer dependencies, tree-shakable builds
106
+ - **Per-request defaults** — Resolve auth, params, timeouts, retry settings, and fetch options from request context
107
+ - **Tiny runtime footprint** — one tiny Standard Schema type dependency, tree-shakable builds
116
108
  - **TypeScript-first** — Full type inference and `IntelliSense` support
117
109
 
118
- **1000fetches** combines native `fetch` performance with enterprise-grade features and bulletproof type safety.
110
+ **1000fetches** combines native `fetch` performance with production-oriented features and schema-backed type safety.
119
111
 
120
112
  ## ➡️ Usage Examples
121
113
 
@@ -130,44 +122,58 @@ const api = createHttpClient({
130
122
  // Or dynamic auth
131
123
  const api = createHttpClient({
132
124
  baseUrl: 'https://api.example.com',
133
- onRequestMiddleware: async context => {
125
+ defaults: async () => {
134
126
  const token = await getToken()
135
- context.headers.set('Authorization', `Bearer ${token}`)
136
- return context
127
+ return {
128
+ headers: { Authorization: `Bearer ${token}` },
129
+ }
137
130
  },
138
131
  })
139
132
  ```
140
133
 
141
- ### Streaming
134
+ ### Dynamic Defaults
142
135
 
143
136
  ```ts
144
- // Upload progress with actual data chunks
145
- await api.post('/api/files', fileData, {
146
- onUploadStreaming: ({ chunk, transferredBytes, totalBytes }) => {
147
- const progress = Math.round((transferredBytes / totalBytes) * 100)
148
- console.log(`Uploading: ${progress}% - chunk size: ${chunk.length} bytes`)
149
- },
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
+ }),
150
145
  })
146
+ ```
151
147
 
152
- // Download progress
153
- await api.get('/api/files/:id', {
154
- pathParams: { id: '123' },
155
- onDownloadStreaming: ({ chunk, transferredBytes, totalBytes }) => {
156
- const progress = Math.round((transferredBytes / totalBytes) * 100)
157
- 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
158
160
  },
159
161
  })
160
162
  ```
161
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
+
162
166
  ### Error Handling
163
167
 
164
168
  ```ts
165
169
  import {
170
+ AbortError,
166
171
  HttpError,
167
172
  NetworkError,
168
173
  TimeoutError,
169
174
  MiddlewareError,
170
175
  PathParameterError,
176
+ SchemaValidationError,
171
177
  } from '1000fetches'
172
178
 
173
179
  try {
@@ -175,7 +181,7 @@ try {
175
181
  .get('/users/:id', {
176
182
  pathParams: { id: '123' },
177
183
  })
178
- .schema(userSchema)
184
+ .contract(userSchema)
179
185
  } catch (error) {
180
186
  if (error instanceof HttpError) {
181
187
  console.log(`HTTP ${error.status}: ${error.statusText}`)
@@ -183,10 +189,14 @@ try {
183
189
  console.log('Network error:', error.message)
184
190
  } else if (error instanceof TimeoutError) {
185
191
  console.log('Request timed out')
192
+ } else if (error instanceof AbortError) {
193
+ console.log('Request was cancelled')
186
194
  } else if (error instanceof MiddlewareError) {
187
195
  console.log('Middleware error:', error.message)
188
196
  } else if (error instanceof PathParameterError) {
189
197
  console.log('Path parameter error:', error.message)
198
+ } else if (error instanceof SchemaValidationError) {
199
+ console.log('Schema validation error:', error.message)
190
200
  }
191
201
  }
192
202
  ```
@@ -198,24 +208,28 @@ try {
198
208
  Creates a new HTTP client with optional configuration.
199
209
 
200
210
  ```ts
201
- function createHttpClient(config?: HttpClientConfig): HttpClient
211
+ function createHttpClient(config?: HttpClientConfig)
202
212
  ```
203
213
 
204
214
  **Configuration Options:**
205
215
 
206
- | Option | Type | Description |
207
- | ---------------------- | --------------------------------------------- | ------------------------------- |
208
- | `baseUrl` | `string` | Base URL for all requests |
209
- | `headers` | `Record<string, string>` | Default headers |
210
- | `timeout` | `number` | Default timeout in milliseconds |
211
- | `retryOptions` | `RetryOptions` | Default retry configuration |
212
- | `onRequestMiddleware` | `(context: RequestContext) => RequestContext` | Request middleware |
213
- | `onResponseMiddleware` | `(response: ResponseType) => ResponseType` | Response middleware |
214
- | `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 |
215
229
 
216
230
  ### <samp>HTTP Methods</samp>
217
231
 
218
- All HTTP methods support schema validation and data extraction:
232
+ All HTTP methods support response contracts and data extraction:
219
233
 
220
234
  ```ts
221
235
  // GET
@@ -223,15 +237,12 @@ const user = await api
223
237
  .get('/users/:id', {
224
238
  pathParams: { id: '123' },
225
239
  })
226
- .schema(userSchema)
240
+ .contract(userSchema)
227
241
  .data()
228
242
  // user 👉 Fully typed user object
229
243
 
230
244
  // POST
231
- const newUser = await api
232
- .post('/users', userData)
233
- .schema(userSchema)
234
- .data()
245
+ const newUser = await api.post('/users', userData).contract(userSchema).data()
235
246
  // newUser 👉 Fully typed user object
236
247
 
237
248
  // PUT
@@ -239,7 +250,7 @@ const updatedUser = await api
239
250
  .put('/users/:id', userData, {
240
251
  pathParams: { id: '123' },
241
252
  })
242
- .schema(userSchema)
253
+ .contract(userSchema)
243
254
  .data()
244
255
  // updatedUser 👉 Fully typed user object
245
256
 
@@ -248,7 +259,7 @@ const patchedUser = await api
248
259
  .patch('/users/:id', partialData, {
249
260
  pathParams: { id: '123' },
250
261
  })
251
- .schema(userSchema)
262
+ .contract(userSchema)
252
263
  .data()
253
264
  // patchedUser 👉 Fully typed user object
254
265
 
@@ -260,23 +271,53 @@ await api.delete('/users/:id', {
260
271
 
261
272
  ### <samp>Request Options</samp>
262
273
 
263
- | Option | Type | Description |
264
- | --------------------- | ---------------------------------------------------------- | --------------------------------- |
265
- | `pathParams` | `Record<string, string \| number>` | Path parameters for URL templates |
266
- | `params` | `Record<string, string \| number \| boolean \| undefined>` | Query parameters |
267
- | `headers` | `Record<string, string>` | Request headers |
268
- | `body` | `any` | Request body |
269
- | `timeout` | `number` | Request timeout |
270
- | `signal` | `AbortSignal` | Request cancellation signal |
271
- | `validateStatus` | `(status: number) => boolean` | Custom status validation |
272
- | `responseType` | `'text' \| 'blob' \| 'arrayBuffer'` | Response type override |
273
- | `cache` | `RequestCache` | Cache mode |
274
- | `credentials` | `RequestCredentials` | Credentials mode |
275
- | `mode` | `RequestMode` | Request mode |
276
- | `redirect` | `RequestRedirect` | Redirect mode |
277
- | `retryOptions` | `RetryOptions` | Retry configuration |
278
- | `onUploadStreaming` | `(event: UploadStreamingEvent) => void` | Upload streaming callback |
279
- | `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 |
289
+
290
+ ### <samp>Retry Options</samp>
291
+
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.
293
+
294
+ ```ts
295
+ const api = createHttpClient({
296
+ retry: {
297
+ maxRetries: 3,
298
+ retryDelay: 250,
299
+ jitter: 'full',
300
+ onRetry: event => {
301
+ console.log(event.method, event.url, event.nextAttempt)
302
+ },
303
+ },
304
+ })
305
+ ```
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
+ ```
280
321
 
281
322
  ### <samp>Response Object</samp>
282
323
 
@@ -287,39 +328,59 @@ interface ResponseType<T> {
287
328
  data: T // Parsed response data
288
329
  status: number // HTTP status code
289
330
  statusText: string // HTTP status text
290
- headers: Record<string, string> // Response headers
291
- method: HttpMethod // HTTP method used
331
+ headers: HttpHeaders // Response headers
332
+ method: HttpMethod
292
333
  url: string // Final URL
293
334
  raw: Response // Raw fetch Response
294
335
  }
295
336
  ```
296
337
 
297
- ### <samp>Schema Validation</samp>
338
+ ### <samp>Response Contracts</samp>
298
339
 
299
- The library provides a chainable API for schema validation:
340
+ The library provides a chainable API for response contracts:
300
341
 
301
342
  ```ts
302
- // Without schema
343
+ // Without contract
303
344
  const response = await api.get('/users/:id', {
304
345
  pathParams: { id: '123' },
305
346
  })
306
347
  // response 👉 ResponseType<unknown>
307
348
 
308
- // With schema
349
+ // With a schema contract
309
350
  const response = await api
310
351
  .get('/users/:id', {
311
352
  pathParams: { id: '123' },
312
353
  })
313
- .schema(userSchema)
354
+ .contract(userSchema)
314
355
  // response 👉 ResponseType<User>
315
356
  ```
316
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
+
317
378
  ### <samp>Data Extraction</samp>
318
379
 
319
380
  The method-based API provides clean data extraction with full type safety:
320
381
 
321
382
  ```ts
322
- // Extract untyped data (without schema)
383
+ // Extract untyped data (without contract)
323
384
  const data = await api
324
385
  .get('/users/:id', {
325
386
  pathParams: { id: '123' },
@@ -327,45 +388,43 @@ const data = await api
327
388
  .data()
328
389
  // data 👉 unknown
329
390
 
330
- // Direct typed data extraction (with schema)
391
+ // Direct typed data extraction (with contract)
331
392
  const user = await api
332
393
  .get('/users/:id', {
333
394
  pathParams: { id: '123' },
334
395
  })
335
- .schema(userSchema)
396
+ .contract(userSchema)
336
397
  .data()
337
398
  // user 👉 { id: number, ... }
338
399
 
339
400
  // Works with all HTTP methods
340
- const newUser = await api
341
- .post('/users', userData)
342
- .schema(userSchema)
343
- .data()
401
+ const newUser = await api.post('/users', userData).contract(userSchema).data()
344
402
  // newUser 👉 Fully typed user object
345
403
 
346
404
  const updatedUser = await api
347
405
  .put('/users/:id', userData, { pathParams: { id: '123' } })
348
- .schema(userSchema)
406
+ .contract(userSchema)
349
407
  .data()
350
408
  // updatedUser 👉 Fully typed user object
409
+
410
+ // Run the request and intentionally ignore response data
411
+ await api.delete('/users/:id', { pathParams: { id: '123' } }).void()
351
412
  ```
352
413
 
353
- ## ➡️ Feature Comparison
354
-
355
- **1000fetches vs Popular Alternatives**
356
-
357
- | Feature | 1000fetches | Axios | Better-fetch | Up-fetch | Native Fetch |
358
- | --------------------- | ----------------- | --------------- | ------------------ | ------------------ | ------------------ |
359
- | **Bundle Size (gz)** | ≈4.3 kB | ≈14.75 kB | ≈3.07 kB | ≈1.6 kB | 0 kB |
360
- | **TypeScript** | Full inference | ⚠️ Limited | ⚠️ Limited | ✅ Good | ❌ Manual |
361
- | **Path Params** | Compile-time | Manual | Manual | Manual | ❌ Manual |
362
- | **Schema Validation** | ✅ Multi-library | None | ⚠️ Limited | Multi-library | ❌ None |
363
- | **Retry Logic** | Built-in | Built-in | Manual | Built-in | Manual |
364
- | **Error Handling** | Structured | Good | ⚠️ Basic | Good | ⚠️ Verbose |
365
- | **Middleware** | Full support | Full support | ⚠️ Limited | ✅ Lifecycle hooks | ❌ None |
366
- | **Streaming** | Real chunks | ❌ None | ❌ None | ✅ Real chunks | ✅ Native |
367
- | **API Design** | ✅ Method-based | ✅ Method-based | ❌ Single function | ❌ Single function | ❌ Single function |
368
- | **Tree Shaking** | ✅ Good | ⚠️ Partial | ✅ Good | ✅ Perfect | ✅ |
414
+ ## ➡️ Design Trade-Offs
415
+
416
+ 1000fetches is intentionally small, but it is not just a thin `fetch` alias. It focuses on the features that tend to become repetitive in application code:
417
+
418
+ | Capability | What 1000fetches Provides |
419
+ | --------------------- | --------------------------------------------------------------------- |
420
+ | **TypeScript** | Schema-inferred response types and typed path parameters |
421
+ | **Path Params** | Compile-time and runtime checks for `:param` placeholders |
422
+ | **Schema Validation** | Standard Schema support for Zod, Valibot, ArkType, and more |
423
+ | **Retry Logic** | Opt-in retry policy with backoff, jitter, and `Retry-After` |
424
+ | **Error Handling** | Structured errors for HTTP, network, timeout, and validation failures |
425
+ | **Dynamic Defaults** | Per-request headers, params, timeout, retry, and fetch options |
426
+ | **Middleware** | Request and response middleware with typed context |
427
+ | **Tree Shaking** | Side-effect-free ESM/CJS builds |
369
428
 
370
429
  ---
371
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 ADDED
@@ -0,0 +1,83 @@
1
+ import { SchemaValidator } from './schema';
2
+ import { CustomFetch, FetchOptions, HttpHeaders, HttpMethod, RequestContext, RequestParamsType, ResponseType, RetryOptions, SerializeBody, SerializeParams } from './types';
3
+ export interface HttpRequestOptions<TBody = unknown, TParams extends RequestParamsType = RequestParamsType> {
4
+ method?: HttpMethod;
5
+ headers?: HttpHeaders;
6
+ /** Query parameters */
7
+ params?: TParams;
8
+ /** Path parameters for URL template */
9
+ pathParams?: Record<string, string | number | undefined>;
10
+ /** Request body */
11
+ body?: TBody;
12
+ timeout?: number;
13
+ signal?: AbortSignal;
14
+ /** Fetch options */
15
+ credentials?: RequestCredentials;
16
+ cache?: RequestCache;
17
+ mode?: RequestMode;
18
+ redirect?: RequestRedirect;
19
+ /** Additional Fetch/Node options passed through to the underlying fetch call */
20
+ fetchOptions?: FetchOptions;
21
+ /** Response type override */
22
+ responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData';
23
+ /** Per-request retry configuration */
24
+ retry?: RetryOptions | boolean;
25
+ }
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> {
55
+ /** Base URL for all requests */
56
+ baseUrl?: string;
57
+ /** Default headers */
58
+ headers?: HttpHeaders;
59
+ /** Default timeout */
60
+ timeout?: number;
61
+ /** Schema validator */
62
+ schemaValidator?: SchemaValidator;
63
+ /** Default retry configuration */
64
+ retry?: RetryOptions | boolean;
65
+ /** Custom fetch implementation */
66
+ fetch?: CustomFetch;
67
+ /** Custom body serializer */
68
+ serializeBody?: SerializeBody<TSerializedBody>;
69
+ /** Custom params serializer */
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>;
73
+ /** Request middleware - can modify request before sending */
74
+ onRequestMiddleware?: <TBody = unknown>(context: RequestContext<TBody>) => RequestContext<TBody> | void | Promise<RequestContext<TBody> | void>;
75
+ /** Response middleware - can modify response after receiving */
76
+ onResponseMiddleware?: (response: ResponseType<unknown>) => ResponseType<unknown> | Promise<ResponseType<unknown>>;
77
+ }
78
+ /**
79
+ * Creates an HTTP request handler with the given configuration.
80
+ * This is the core function that handles all HTTP requests with retry logic,
81
+ * middleware, serialization, defaults, and timeouts.
82
+ */
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>>;