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.
- package/LICENSE +1 -1
- package/README.md +168 -109
- package/dist/client/index.d.ts +23 -0
- package/dist/contract/index.d.ts +6 -0
- package/dist/core.d.ts +83 -0
- package/dist/errors/errors.d.ts +111 -0
- package/dist/errors/handling.d.ts +2 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +7 -347
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- 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/index.d.ts +35 -0
- package/dist/status.d.ts +2 -0
- package/dist/types.d.ts +150 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/path.d.ts +71 -0
- package/docs/BEST_PRACTICES.md +685 -0
- package/package.json +52 -56
- 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/index.cjs.js +0 -2
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.es.js +0 -793
- package/dist/index.es.js.map +0 -1
package/LICENSE
CHANGED
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
|
-
-
|
|
34
|
-
- 🔁 **Retries, timeouts, middleware** — production essentials
|
|
35
|
-
- 🎯 **Method-based API** — `api.get()` with
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
75
|
+
.contract(userSchema)
|
|
75
76
|
.data()
|
|
76
77
|
// user 👉 { id: number, ... }
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
|
|
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** —
|
|
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
|
-
- **
|
|
106
|
-
- **Minimalistic footprint** —
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
115
|
-
- **
|
|
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
|
|
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
|
-
|
|
125
|
+
defaults: async () => {
|
|
134
126
|
const token = await getToken()
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
return {
|
|
128
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
129
|
+
}
|
|
137
130
|
},
|
|
138
131
|
})
|
|
139
132
|
```
|
|
140
133
|
|
|
141
|
-
###
|
|
134
|
+
### Dynamic Defaults
|
|
142
135
|
|
|
143
136
|
```ts
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
.
|
|
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)
|
|
211
|
+
function createHttpClient(config?: HttpClientConfig)
|
|
202
212
|
```
|
|
203
213
|
|
|
204
214
|
**Configuration Options:**
|
|
205
215
|
|
|
206
|
-
| Option | Type
|
|
207
|
-
| ---------------------- |
|
|
208
|
-
| `baseUrl` | `string`
|
|
209
|
-
| `headers` | `Record<string, string>`
|
|
210
|
-
| `timeout` | `number`
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
264
|
-
|
|
|
265
|
-
| `pathParams`
|
|
266
|
-
| `params`
|
|
267
|
-
| `headers`
|
|
268
|
-
| `body`
|
|
269
|
-
| `timeout`
|
|
270
|
-
| `signal`
|
|
271
|
-
| `
|
|
272
|
-
| `
|
|
273
|
-
| `
|
|
274
|
-
| `
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
| `
|
|
278
|
-
|
|
279
|
-
|
|
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:
|
|
291
|
-
method: HttpMethod
|
|
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>
|
|
338
|
+
### <samp>Response Contracts</samp>
|
|
298
339
|
|
|
299
|
-
The library provides a chainable API for
|
|
340
|
+
The library provides a chainable API for response contracts:
|
|
300
341
|
|
|
301
342
|
```ts
|
|
302
|
-
// Without
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
## ➡️
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
|
358
|
-
| --------------------- |
|
|
359
|
-
| **
|
|
360
|
-
| **
|
|
361
|
-
| **
|
|
362
|
-
| **
|
|
363
|
-
| **
|
|
364
|
-
| **
|
|
365
|
-
| **Middleware** |
|
|
366
|
-
| **
|
|
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>>;
|