1000fetches 0.2.0 → 0.2.2
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 +94 -68
- package/dist/client-C7dvgNnD.js +2 -0
- package/dist/client-C7dvgNnD.js.map +1 -0
- package/dist/client-DOv6m2TJ.mjs +2 -0
- package/dist/client-DOv6m2TJ.mjs.map +1 -0
- package/dist/client.d.ts +22 -0
- package/dist/core.d.ts +57 -0
- package/dist/errors.d.ts +76 -0
- package/dist/http.cjs +2 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.mjs +2 -0
- package/dist/http.mjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +15 -346
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/schema.d.ts +35 -0
- package/dist/types.d.ts +145 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/path.d.ts +73 -0
- package/dist/utils/streaming.d.ts +9 -0
- package/docs/API.md +754 -0
- package/docs/BEST_PRACTICES.md +606 -0
- package/docs/MIGRATION.md +719 -0
- package/package.json +40 -53
- package/dist/index.cjs.js +0 -2
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.es.js +0 -787
- package/dist/index.es.js.map +0 -1
package/docs/API.md
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
# 1000fetches API Documentation
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [HttpClient](#httpclient)
|
|
6
|
+
- [Configuration Options](#configuration-options)
|
|
7
|
+
- [HTTP Methods](#http-methods)
|
|
8
|
+
- [Response Methods](#response-methods)
|
|
9
|
+
- [Schema Validation](#schema-validation)
|
|
10
|
+
- [Error Handling](#error-handling)
|
|
11
|
+
- [Middleware](#middleware)
|
|
12
|
+
- [Retry Logic](#retry-logic)
|
|
13
|
+
- [Type Safety](#type-safety)
|
|
14
|
+
|
|
15
|
+
## HttpClient
|
|
16
|
+
|
|
17
|
+
The main HTTP client that provides a fluent API for making HTTP requests with optional schema validation.
|
|
18
|
+
|
|
19
|
+
### Constructor
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
createHttpClient(config?: HttpClientConfig)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Parameters:**
|
|
26
|
+
|
|
27
|
+
- `config` (optional): Configuration options for the HTTP client
|
|
28
|
+
|
|
29
|
+
**Example:**
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createHttpClient } from '1000fetches'
|
|
33
|
+
|
|
34
|
+
const client = createHttpClient({
|
|
35
|
+
baseUrl: 'https://api.example.com',
|
|
36
|
+
timeout: 5_000,
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: 'Bearer your-token',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Or use the shortcut instance
|
|
43
|
+
import http from '1000fetches/http'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`1000fetches/http` intentionally exposes only a default export. Use the root `1000fetches` entrypoint for `createHttpClient`, error classes, and public types.
|
|
47
|
+
|
|
48
|
+
## Configuration Options
|
|
49
|
+
|
|
50
|
+
### HttpClientConfig
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
interface HttpClientConfig {
|
|
54
|
+
baseUrl?: string
|
|
55
|
+
timeout?: number
|
|
56
|
+
headers?: HttpHeaders
|
|
57
|
+
retry?: RetryOptions | boolean
|
|
58
|
+
schemaValidator?: SchemaValidator
|
|
59
|
+
fetch?: CustomFetch
|
|
60
|
+
serializeBody?: SerializeBody
|
|
61
|
+
serializeParams?: SerializeParams
|
|
62
|
+
onRequestMiddleware?: <TBody = unknown>(
|
|
63
|
+
context: RequestContext<TBody>
|
|
64
|
+
) => RequestContext<TBody> | Promise<RequestContext<TBody>>
|
|
65
|
+
onResponseMiddleware?: (
|
|
66
|
+
response: ResponseType<unknown>
|
|
67
|
+
) => ResponseType<unknown> | Promise<ResponseType<unknown>>
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Properties:**
|
|
72
|
+
|
|
73
|
+
- **`baseUrl`** (optional): Base URL for all requests. Must be an absolute URL or a root-relative path starting with `/`. Native Node.js `fetch` still requires absolute URLs unless you provide a custom `fetch` implementation.
|
|
74
|
+
- **`timeout`** (optional): Request timeout in milliseconds (default: 30_000)
|
|
75
|
+
- **`headers`** (optional): Default headers to include with every request
|
|
76
|
+
- **`retry`** (optional): Opt-in retry configuration for failed requests. Use `true` for the default retry policy, `false` to disable retry, or a `RetryOptions` object for custom retry behavior.
|
|
77
|
+
- **`schemaValidator`** (optional): Custom schema validator
|
|
78
|
+
- **`fetch`** (optional): Custom fetch implementation
|
|
79
|
+
- **`serializeBody`** (optional): Custom body serializer
|
|
80
|
+
- **`serializeParams`** (optional): Custom params serializer
|
|
81
|
+
- **`onRequestMiddleware`** (optional): Request middleware for modifying requests
|
|
82
|
+
- **`onResponseMiddleware`** (optional): Response middleware for modifying responses
|
|
83
|
+
|
|
84
|
+
### RetryOptions
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface RetryOptions {
|
|
88
|
+
maxRetries?: number
|
|
89
|
+
retryDelay?: number
|
|
90
|
+
backoffFactor?: number
|
|
91
|
+
retryStatusCodes?: number[]
|
|
92
|
+
retryNetworkErrors?: boolean
|
|
93
|
+
maxRetryDelay?: number
|
|
94
|
+
retryMethods?: readonly HttpMethod[]
|
|
95
|
+
retryUnsafeMethods?: boolean
|
|
96
|
+
jitter?:
|
|
97
|
+
| 'none'
|
|
98
|
+
| 'full'
|
|
99
|
+
| 'equal'
|
|
100
|
+
| ((delay: number, retryCount: number) => number)
|
|
101
|
+
respectRetryAfter?: boolean
|
|
102
|
+
onRetry?: (event: RetryEvent) => void | Promise<void>
|
|
103
|
+
shouldRetry?: (
|
|
104
|
+
error: Error,
|
|
105
|
+
retryCount: number,
|
|
106
|
+
context: RetryContext
|
|
107
|
+
) => boolean | Promise<boolean>
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Retries are disabled by default. Set client `retry`, request `retry: true`, or per-request retry settings to enable them. The built-in policy retries idempotent methods by default and skips one-shot stream bodies.
|
|
112
|
+
|
|
113
|
+
**Properties:**
|
|
114
|
+
|
|
115
|
+
- **`maxRetries`**: Maximum number of retry attempts when retries are enabled (default: 3)
|
|
116
|
+
- **`retryDelay`**: Initial delay between retries in milliseconds when retries are enabled (default: 300)
|
|
117
|
+
- **`backoffFactor`**: Exponential backoff multiplier (default: 2)
|
|
118
|
+
- **`retryStatusCodes`**: HTTP status codes that trigger retries when retries are enabled (default: [408, 429, 500, 502, 503, 504])
|
|
119
|
+
- **`retryNetworkErrors`**: Whether to retry on network errors when retries are enabled (default: true)
|
|
120
|
+
- **`maxRetryDelay`**: Maximum delay between retries when retries are enabled (default: 30_000)
|
|
121
|
+
- **`retryMethods`**: Methods eligible for built-in retries (default: GET, HEAD, OPTIONS, PUT, DELETE)
|
|
122
|
+
- **`retryUnsafeMethods`**: Allow built-in retries for methods outside `retryMethods` (default: false)
|
|
123
|
+
- **`jitter`**: Optional jitter strategy for exponential backoff delays (default: 'none')
|
|
124
|
+
- **`respectRetryAfter`**: Honor `Retry-After` headers on HTTP errors (default: true)
|
|
125
|
+
- **`onRetry`**: Hook called before waiting for the next attempt
|
|
126
|
+
- **`shouldRetry`**: Custom function to determine if a request should be retried
|
|
127
|
+
|
|
128
|
+
## HTTP Methods
|
|
129
|
+
|
|
130
|
+
All HTTP methods return a response object that allows you to optionally attach schema validation.
|
|
131
|
+
|
|
132
|
+
### GET Request
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
client.get<Path, TParams>(url: Path, options?: EnforcedPathParamsOptions<never, TParams, Path>): SchemaableResponse<unknown>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Example:**
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Basic GET request - returns unknown data
|
|
142
|
+
const users = await client.get('/users')
|
|
143
|
+
// users.data 👉 unknown
|
|
144
|
+
|
|
145
|
+
// GET with path parameters
|
|
146
|
+
const user = await client.get('/users/:id', {
|
|
147
|
+
pathParams: { id: '123' },
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// GET with query parameters
|
|
151
|
+
const filteredUsers = await client.get('/users', {
|
|
152
|
+
params: {
|
|
153
|
+
page: 1,
|
|
154
|
+
limit: 10,
|
|
155
|
+
status: 'active',
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// GET with schema validation
|
|
160
|
+
import { z } from 'zod'
|
|
161
|
+
|
|
162
|
+
const userSchema = z.object({
|
|
163
|
+
id: z.number(),
|
|
164
|
+
name: z.string(),
|
|
165
|
+
email: z.string(),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const validatedUser = await client.get('/users/1').schema(userSchema)
|
|
169
|
+
// validatedUser.data 👉 { id: number, name: string, email: string }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### POST Request
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
client.post<Path, TBody, TParams>(url: Path, body?: TBody, options?: EnforcedPathParamsOptions<TBody, TParams, Path>): SchemaableResponse<unknown>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Example:**
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Basic POST request - returns unknown data
|
|
182
|
+
const newUser = await client.post('/users', {
|
|
183
|
+
name: 'John Doe',
|
|
184
|
+
email: 'john@example.com',
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// POST with schema
|
|
188
|
+
const createUserSchema = z.object({
|
|
189
|
+
name: z.string(),
|
|
190
|
+
email: z.string(),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const responseSchema = z.object({
|
|
194
|
+
id: z.number(),
|
|
195
|
+
name: z.string(),
|
|
196
|
+
email: z.string(),
|
|
197
|
+
createdAt: z.string(),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const validatedUser = await client
|
|
201
|
+
.post('/users', userData)
|
|
202
|
+
.schema(responseSchema)
|
|
203
|
+
// validatedUser.data 👉 { id: number, name: string, email: string, createdAt: string }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### PUT Request
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
client.put<Path, TBody, TParams>(url: Path, body?: TBody, options?: EnforcedPathParamsOptions<TBody, TParams, Path>): SchemaableResponse<unknown>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### PATCH Request
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
client.patch<Path, TBody, TParams>(url: Path, body?: TBody, options?: EnforcedPathParamsOptions<TBody, TParams, Path>): SchemaableResponse<unknown>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### DELETE Request
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
client.delete<Path, TParams>(url: Path, options?: EnforcedPathParamsOptions<never, TParams, Path>): SchemaableResponse<unknown>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Generic Request
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
client.request<Path, TBody, TParams>(url: Path, options?: HttpRequestOptions<TBody, TParams>): SchemaableResponse<unknown>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Example:**
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// HEAD request
|
|
234
|
+
const headResponse = await client.request('/users/1', { method: 'HEAD' })
|
|
235
|
+
|
|
236
|
+
// OPTIONS request
|
|
237
|
+
const optionsResponse = await client.request('/users', { method: 'OPTIONS' })
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Response Methods
|
|
241
|
+
|
|
242
|
+
All HTTP methods return a `SchemaableResponse<unknown>` that provides extraction helpers:
|
|
243
|
+
|
|
244
|
+
### SchemaableResponse Interface
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
interface SchemaableResponse<T> extends Promise<ResponseType<T>> {
|
|
248
|
+
schema<S extends Schema>(schema: S): ExtractableResponse<InferSchemaOutput<S>>
|
|
249
|
+
data(): Promise<T>
|
|
250
|
+
void(): Promise<void>
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Methods
|
|
255
|
+
|
|
256
|
+
#### `.schema(schema)`
|
|
257
|
+
|
|
258
|
+
Validates the response data against a schema and returns a typed response.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const response = await client.get('/users/1').schema(userSchema)
|
|
262
|
+
// awaited response 👉 ResponseType<User>
|
|
263
|
+
// response.data 👉 User
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### `.data()`
|
|
267
|
+
|
|
268
|
+
Extracts the data from the response without schema validation.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const data = await client.get('/users/1').data()
|
|
272
|
+
// data 👉 unknown
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### `.void()`
|
|
276
|
+
|
|
277
|
+
Runs the request and resolves without returning response data. Errors and schema validation still run.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
await client.delete('/users/1').void()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Design Philosophy
|
|
284
|
+
|
|
285
|
+
1. **Fetch unknown data once** - All requests return `unknown` data by default
|
|
286
|
+
2. **Schema changes type** - If `.schema()` is called, it validates and types the data
|
|
287
|
+
3. **No schema = unknown data** - If no schema is provided, data remains `unknown`
|
|
288
|
+
4. **Single HTTP request** - Schema validation happens client-side, no duplicate requests
|
|
289
|
+
|
|
290
|
+
## Error Handling
|
|
291
|
+
|
|
292
|
+
### Error Types
|
|
293
|
+
|
|
294
|
+
The library provides structured error types for different failure scenarios:
|
|
295
|
+
|
|
296
|
+
#### HttpError
|
|
297
|
+
|
|
298
|
+
Thrown when an HTTP request fails with a non-2xx status code.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
class HttpError extends Error {
|
|
302
|
+
readonly status: number
|
|
303
|
+
readonly statusText: string
|
|
304
|
+
readonly data: unknown
|
|
305
|
+
readonly response: Response
|
|
306
|
+
readonly url: string
|
|
307
|
+
readonly method: string
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### NetworkError
|
|
312
|
+
|
|
313
|
+
Thrown when a network-level error occurs.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
class NetworkError extends Error {
|
|
317
|
+
readonly cause?: Error
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### TimeoutError
|
|
322
|
+
|
|
323
|
+
Thrown when a request exceeds the timeout limit.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
class TimeoutError extends Error {
|
|
327
|
+
readonly cause?: Error
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### SchemaValidationError
|
|
332
|
+
|
|
333
|
+
Thrown when response data fails schema validation.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
class SchemaValidationError extends Error {
|
|
337
|
+
readonly schema: unknown
|
|
338
|
+
readonly data: unknown
|
|
339
|
+
readonly issues?: ReadonlyArray<StandardSchemaV1.Issue>
|
|
340
|
+
readonly cause?: Error
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### MiddlewareError
|
|
345
|
+
|
|
346
|
+
Thrown when middleware fails.
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
class MiddlewareError extends Error {
|
|
350
|
+
readonly type: 'request' | 'response'
|
|
351
|
+
readonly url?: string
|
|
352
|
+
readonly method?: string
|
|
353
|
+
readonly cause?: Error
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### PathParameterError
|
|
358
|
+
|
|
359
|
+
Thrown when required path parameters are missing.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
class PathParameterError extends Error {
|
|
363
|
+
readonly url: string
|
|
364
|
+
readonly requiredParams: string[]
|
|
365
|
+
readonly providedParams: string[]
|
|
366
|
+
readonly cause?: Error
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### SerializationError
|
|
371
|
+
|
|
372
|
+
Thrown when request serialization or successful-response parsing fails.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
class SerializationError extends Error {
|
|
376
|
+
readonly cause?: Error
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### InvalidSchemaError
|
|
381
|
+
|
|
382
|
+
Thrown when `.schema()` receives a value that does not implement Standard Schema v1.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
class InvalidSchemaError extends Error {
|
|
386
|
+
readonly schema: unknown
|
|
387
|
+
readonly cause?: Error
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
#### InvalidBaseUrlError
|
|
392
|
+
|
|
393
|
+
Thrown when `baseUrl` is not an absolute URL or root-relative path.
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
class InvalidBaseUrlError extends Error {
|
|
397
|
+
readonly baseUrl: string
|
|
398
|
+
readonly cause?: Error
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### AsyncSchemaValidationError
|
|
403
|
+
|
|
404
|
+
Exported for custom schema integrations that need a structured async-schema validation error.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
class AsyncSchemaValidationError extends Error {
|
|
408
|
+
readonly schema: unknown
|
|
409
|
+
readonly cause?: Error
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Error Handling Examples
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import {
|
|
417
|
+
HttpError,
|
|
418
|
+
NetworkError,
|
|
419
|
+
TimeoutError,
|
|
420
|
+
SchemaValidationError,
|
|
421
|
+
} from '1000fetches'
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
await client.get('/users/1')
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (error instanceof HttpError) {
|
|
427
|
+
console.log(`HTTP Error: ${error.status} ${error.statusText}`)
|
|
428
|
+
console.log(`Request: ${error.method} ${error.url}`)
|
|
429
|
+
console.log('Response data:', error.data)
|
|
430
|
+
} else if (error instanceof NetworkError) {
|
|
431
|
+
console.log('Network error occurred')
|
|
432
|
+
} else if (error instanceof TimeoutError) {
|
|
433
|
+
console.log('Request timed out')
|
|
434
|
+
} else if (error instanceof SchemaValidationError) {
|
|
435
|
+
console.log('Response data validation failed')
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Middleware
|
|
441
|
+
|
|
442
|
+
Middleware allows you to modify requests before they are sent and responses before they are returned.
|
|
443
|
+
|
|
444
|
+
### Request Middleware
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
const client = createHttpClient({
|
|
448
|
+
onRequestMiddleware: async (context: RequestContext) => {
|
|
449
|
+
// Modify the request context
|
|
450
|
+
context.headers.set('X-Custom-Header', 'value')
|
|
451
|
+
return context
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Response Middleware
|
|
457
|
+
|
|
458
|
+
Response middleware receives the parsed response before HTTP status handling. The returned `ResponseType` is authoritative: if middleware changes `status`, that changed status controls whether the request resolves or throws `HttpError`, and it also affects retry decisions.
|
|
459
|
+
|
|
460
|
+
Use this deliberately for explicit recovery or status normalization. For example, a middleware can turn a `404` response into a successful `null` result:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const client = createHttpClient({
|
|
464
|
+
onResponseMiddleware: async response => {
|
|
465
|
+
if (response.status === 404) {
|
|
466
|
+
return {
|
|
467
|
+
...response,
|
|
468
|
+
status: 200,
|
|
469
|
+
statusText: 'Recovered Not Found',
|
|
470
|
+
data: null,
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return response
|
|
475
|
+
},
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const user = await client.get('/users/missing').data()
|
|
479
|
+
// user 👉 null
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
If middleware only needs to normalize error payloads, preserve `status` so the client still throws for real non-2xx responses:
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
const client = createHttpClient({
|
|
486
|
+
onResponseMiddleware: async (response: ResponseType<unknown>) => {
|
|
487
|
+
const data =
|
|
488
|
+
response.data && typeof response.data === 'object'
|
|
489
|
+
? response.data
|
|
490
|
+
: { value: response.data }
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
...response,
|
|
494
|
+
data: {
|
|
495
|
+
...data,
|
|
496
|
+
processed: true,
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Middleware Examples
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// Authentication middleware
|
|
507
|
+
const client = createHttpClient({
|
|
508
|
+
onRequestMiddleware: async context => {
|
|
509
|
+
const token = await getAuthToken()
|
|
510
|
+
context.headers.set('Authorization', `Bearer ${token}`)
|
|
511
|
+
return context
|
|
512
|
+
},
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Logging middleware
|
|
516
|
+
const client = createHttpClient({
|
|
517
|
+
onRequestMiddleware: async context => {
|
|
518
|
+
console.log(`Making request: ${context.method} ${context.url}`)
|
|
519
|
+
return context
|
|
520
|
+
},
|
|
521
|
+
onResponseMiddleware: async response => {
|
|
522
|
+
console.log(`Response received: ${response.status} ${response.statusText}`)
|
|
523
|
+
return response
|
|
524
|
+
},
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
// Error handling middleware
|
|
528
|
+
const client = createHttpClient({
|
|
529
|
+
onResponseMiddleware: async response => {
|
|
530
|
+
if (response.status === 401) {
|
|
531
|
+
// Handle unauthorized
|
|
532
|
+
await refreshToken()
|
|
533
|
+
}
|
|
534
|
+
return response
|
|
535
|
+
},
|
|
536
|
+
})
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Schema Validation
|
|
540
|
+
|
|
541
|
+
The library supports schema validation using popular validation libraries through the Standard Schema interface. Schema validation is applied using the `.schema()` method.
|
|
542
|
+
|
|
543
|
+
### Supported Libraries
|
|
544
|
+
|
|
545
|
+
- **Zod**
|
|
546
|
+
- **Valibot**
|
|
547
|
+
- **ArkType**
|
|
548
|
+
- **Custom validators**
|
|
549
|
+
|
|
550
|
+
### Usage Examples
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { z } from 'zod'
|
|
554
|
+
|
|
555
|
+
// Define schemas
|
|
556
|
+
const userSchema = z.object({
|
|
557
|
+
id: z.number(),
|
|
558
|
+
name: z.string(),
|
|
559
|
+
email: z.string(),
|
|
560
|
+
age: z.number().optional(),
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const createUserSchema = z.object({
|
|
564
|
+
name: z.string().min(1),
|
|
565
|
+
email: z.string(),
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
// Response validation
|
|
569
|
+
const user = await client.get('/users/1').schema(userSchema)
|
|
570
|
+
// user.data 👉 { id: number, name: string, email: string, age?: number }
|
|
571
|
+
|
|
572
|
+
// POST with response validation
|
|
573
|
+
const newUser = await client.post('/users', userData).schema(userSchema)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Custom Schema Validator
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
import { createHttpClient } from '1000fetches'
|
|
580
|
+
import type { SchemaValidator } from '1000fetches'
|
|
581
|
+
|
|
582
|
+
const customValidator: SchemaValidator = {
|
|
583
|
+
async validate<T>(schema, data): Promise<T> {
|
|
584
|
+
const result = await schema['~standard'].validate(data)
|
|
585
|
+
|
|
586
|
+
if (result.issues) {
|
|
587
|
+
throw new Error(JSON.stringify(result.issues))
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return result.value
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const client = createHttpClient({
|
|
595
|
+
schemaValidator: customValidator,
|
|
596
|
+
})
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Retry Logic
|
|
600
|
+
|
|
601
|
+
The library includes built-in retry defaults with exponential backoff for handling transient failures when retries are enabled.
|
|
602
|
+
|
|
603
|
+
### Retry Defaults When Enabled
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
const defaultRetryOptions = {
|
|
607
|
+
maxRetries: 3,
|
|
608
|
+
retryDelay: 300,
|
|
609
|
+
backoffFactor: 2,
|
|
610
|
+
retryStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
611
|
+
retryNetworkErrors: true,
|
|
612
|
+
maxRetryDelay: 30_000,
|
|
613
|
+
retryMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'],
|
|
614
|
+
retryUnsafeMethods: false,
|
|
615
|
+
jitter: 'none',
|
|
616
|
+
respectRetryAfter: true,
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Custom Retry Configuration
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
import { NetworkError, createHttpClient } from '1000fetches'
|
|
624
|
+
|
|
625
|
+
const client = createHttpClient({
|
|
626
|
+
baseUrl: 'https://api.example.com',
|
|
627
|
+
retry: {
|
|
628
|
+
maxRetries: 5,
|
|
629
|
+
retryDelay: 1_000,
|
|
630
|
+
backoffFactor: 1.5,
|
|
631
|
+
retryStatusCodes: [500, 502, 503, 504],
|
|
632
|
+
jitter: 'full',
|
|
633
|
+
onRetry: event => {
|
|
634
|
+
console.log(event.method, event.url, event.nextAttempt)
|
|
635
|
+
},
|
|
636
|
+
shouldRetry: (error, retryCount, context) => {
|
|
637
|
+
// Custom retry logic
|
|
638
|
+
return (
|
|
639
|
+
retryCount < 3 &&
|
|
640
|
+
context.bodyReplayable &&
|
|
641
|
+
error instanceof NetworkError
|
|
642
|
+
)
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
})
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Per-Request Retry Configuration
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
// Disable retries for this request
|
|
652
|
+
await client.get('/users', { retry: false })
|
|
653
|
+
|
|
654
|
+
// Custom retry for this request
|
|
655
|
+
await client.get('/users', {
|
|
656
|
+
retry: {
|
|
657
|
+
maxRetries: 1,
|
|
658
|
+
retryDelay: 500,
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## Type Safety
|
|
664
|
+
|
|
665
|
+
The library provides comprehensive TypeScript support with automatic type inference through response methods.
|
|
666
|
+
|
|
667
|
+
### Response Method Types
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
interface User {
|
|
671
|
+
id: number
|
|
672
|
+
name: string
|
|
673
|
+
email: string
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Without schema - data is unknown
|
|
677
|
+
const response = await client.get('/users/1')
|
|
678
|
+
// response.data 👉 unknown
|
|
679
|
+
|
|
680
|
+
// With schema - data is typed
|
|
681
|
+
const user = await client.get('/users/1').schema(userSchema)
|
|
682
|
+
// user.data 👉 User
|
|
683
|
+
|
|
684
|
+
// Array responses
|
|
685
|
+
const users = await client.get('/users').schema(z.array(userSchema))
|
|
686
|
+
// users.data 👉 User[]
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Path Parameter Validation
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
// Compile-time path parameter validation
|
|
693
|
+
await client.get('/users/:id', {
|
|
694
|
+
pathParams: { id: '123' }, // ✓ Valid
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
await client.get('/users/:id', {
|
|
698
|
+
pathParams: { userId: '123' }, // ✗ TypeScript error: 'id' is required
|
|
699
|
+
})
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Optional path params are supported only for trailing final segments like `/users/:id?`.
|
|
703
|
+
Shapes like `/users/:id?/cards/:cardId` are rejected at compile time and runtime.
|
|
704
|
+
|
|
705
|
+
### Request Options Type
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
interface HttpRequestOptions<
|
|
709
|
+
TBody = unknown,
|
|
710
|
+
TParams extends RequestParamsType = RequestParamsType,
|
|
711
|
+
> {
|
|
712
|
+
method?: HttpMethod
|
|
713
|
+
headers?: HttpHeaders
|
|
714
|
+
params?: TParams
|
|
715
|
+
pathParams?: Record<string, string | number | undefined>
|
|
716
|
+
body?: TBody
|
|
717
|
+
timeout?: number
|
|
718
|
+
signal?: AbortSignal
|
|
719
|
+
credentials?: RequestCredentials
|
|
720
|
+
cache?: RequestCache
|
|
721
|
+
mode?: RequestMode
|
|
722
|
+
redirect?: RequestRedirect
|
|
723
|
+
fetchOptions?: FetchOptions
|
|
724
|
+
onUploadStreaming?: (event: UploadStreamingEvent) => void
|
|
725
|
+
onDownloadStreaming?: (event: DownloadStreamingEvent) => void
|
|
726
|
+
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData'
|
|
727
|
+
retry?: RetryOptions | boolean
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Response Types
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
interface ResponseType<T = unknown> {
|
|
735
|
+
data: T
|
|
736
|
+
status: number
|
|
737
|
+
statusText: string
|
|
738
|
+
headers: Record<string, string>
|
|
739
|
+
method: string
|
|
740
|
+
url: string
|
|
741
|
+
raw: Response
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
interface SchemaableResponse<T> extends Promise<ResponseType<T>> {
|
|
745
|
+
schema<S extends Schema>(schema: S): ExtractableResponse<InferSchemaOutput<S>>
|
|
746
|
+
data(): Promise<T>
|
|
747
|
+
void(): Promise<void>
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
interface ExtractableResponse<T> extends Promise<ResponseType<T>> {
|
|
751
|
+
data(): Promise<T>
|
|
752
|
+
void(): Promise<void>
|
|
753
|
+
}
|
|
754
|
+
```
|