@vertz/fetch 0.2.41 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -74
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# @vertz/fetch
|
|
2
2
|
|
|
3
|
-
Type-safe HTTP client for Vertz with automatic retries, streaming support, and flexible authentication strategies.
|
|
3
|
+
Type-safe HTTP client for Vertz with error-as-value semantics, automatic retries, streaming support, and flexible authentication strategies.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
+
- **Error-as-value** — Returns `Result<T, Error>` instead of throwing, for predictable error handling
|
|
7
8
|
- **Type-safe requests** — Full TypeScript inference for request/response types
|
|
8
9
|
- **Automatic retries** — Exponential/linear backoff with configurable retry logic
|
|
9
10
|
- **Streaming support** — Server-Sent Events (SSE) and newline-delimited JSON (NDJSON)
|
|
10
11
|
- **Flexible authentication** — Bearer tokens, Basic auth, API keys, or custom strategies
|
|
11
12
|
- **Request/response hooks** — Intercept and transform at every stage
|
|
12
|
-
- **
|
|
13
|
+
- **Typed error hierarchy** — Specific error classes for all HTTP status codes
|
|
13
14
|
- **Timeout management** — Automatic timeout with AbortSignal support
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
@@ -21,7 +22,7 @@ npm install @vertz/fetch
|
|
|
21
22
|
## Quick Start
|
|
22
23
|
|
|
23
24
|
```typescript
|
|
24
|
-
import { FetchClient } from '@vertz/fetch';
|
|
25
|
+
import { FetchClient, isOk, isErr, unwrap } from '@vertz/fetch';
|
|
25
26
|
|
|
26
27
|
// Create a client with base configuration
|
|
27
28
|
const client = new FetchClient({
|
|
@@ -32,17 +33,25 @@ const client = new FetchClient({
|
|
|
32
33
|
timeoutMs: 5000,
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
// Make a typed GET request
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
// Make a typed GET request — returns Result<T, FetchError>
|
|
37
|
+
const result = await client.get<{ id: number; name: string }>('/users/1');
|
|
38
|
+
|
|
39
|
+
if (isOk(result)) {
|
|
40
|
+
console.log(result.data.data.name); // Fully typed!
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Or unwrap directly (throws if error)
|
|
44
|
+
const { data } = unwrap(await client.get<{ id: number; name: string }>('/users/1'));
|
|
45
|
+
console.log(data.name);
|
|
38
46
|
|
|
39
47
|
// POST with body
|
|
40
|
-
const newUser = await client.
|
|
41
|
-
|
|
48
|
+
const newUser = await client.post<{ id: number }>('/users', {
|
|
49
|
+
name: 'Alice',
|
|
50
|
+
email: 'alice@example.com',
|
|
42
51
|
});
|
|
43
52
|
|
|
44
53
|
// Query parameters
|
|
45
|
-
const users = await client.
|
|
54
|
+
const users = await client.get<{ users: Array<{ id: number }> }>('/users', {
|
|
46
55
|
query: { page: 1, limit: 10 },
|
|
47
56
|
});
|
|
48
57
|
```
|
|
@@ -110,8 +119,21 @@ interface FetchClientConfig {
|
|
|
110
119
|
Make a standard HTTP request with JSON response.
|
|
111
120
|
|
|
112
121
|
```typescript
|
|
113
|
-
const
|
|
114
|
-
|
|
122
|
+
const result = await client.request<User>('GET', '/users/1');
|
|
123
|
+
|
|
124
|
+
if (isOk(result)) {
|
|
125
|
+
const { data, status, headers } = result.data;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
##### Convenience methods
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
client.get<T>(path, options?) // GET request
|
|
133
|
+
client.post<T>(path, body?, options?) // POST request
|
|
134
|
+
client.put<T>(path, body?, options?) // PUT request
|
|
135
|
+
client.patch<T>(path, body?, options?) // PATCH request
|
|
136
|
+
client.delete<T>(path, options?) // DELETE request
|
|
115
137
|
```
|
|
116
138
|
|
|
117
139
|
**Options:**
|
|
@@ -125,19 +147,13 @@ interface RequestOptions {
|
|
|
125
147
|
}
|
|
126
148
|
```
|
|
127
149
|
|
|
128
|
-
**Returns:**
|
|
150
|
+
**Returns:** `Promise<Result<{ data: T; status: number; headers: Headers }, FetchError>>`
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
interface FetchResponse<T> {
|
|
132
|
-
data: T;
|
|
133
|
-
status: number;
|
|
134
|
-
headers: Headers;
|
|
135
|
-
}
|
|
136
|
-
```
|
|
152
|
+
On success, the result is `Ok` with `data`, `status`, and `headers`. On failure, the result is `Err` with a typed `FetchError`.
|
|
137
153
|
|
|
138
154
|
##### `requestStream<T>(options)`
|
|
139
155
|
|
|
140
|
-
Stream responses using SSE or NDJSON format.
|
|
156
|
+
Stream responses using SSE or NDJSON format. Unlike `request()`, streaming errors are thrown (not wrapped in `Result`) since you can't return a `Result` from an async generator mid-stream.
|
|
141
157
|
|
|
142
158
|
```typescript
|
|
143
159
|
for await (const chunk of client.requestStream<LogEntry>({
|
|
@@ -240,43 +256,63 @@ const client = new FetchClient({
|
|
|
240
256
|
|
|
241
257
|
### Error Handling
|
|
242
258
|
|
|
243
|
-
All
|
|
259
|
+
All requests return a `Result<T, FetchError>` — errors are values, not exceptions. Use `isOk()`/`isErr()` to check, or `unwrap()` to extract the value (throws on error).
|
|
244
260
|
|
|
245
261
|
```typescript
|
|
246
|
-
import {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
262
|
+
import { FetchClient, isOk, isErr, unwrap, matchError } from '@vertz/fetch';
|
|
263
|
+
|
|
264
|
+
const result = await client.get<User>('/users/999');
|
|
265
|
+
|
|
266
|
+
// Pattern 1: Check with isOk/isErr
|
|
267
|
+
if (isErr(result)) {
|
|
268
|
+
console.error('Request failed:', result.error.message);
|
|
269
|
+
console.error('Status:', result.error.status);
|
|
270
|
+
} else {
|
|
271
|
+
console.log('User:', result.data.data);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Pattern 2: Unwrap (throws if error)
|
|
275
|
+
const { data: user } = unwrap(await client.get<User>('/users/1'));
|
|
276
|
+
|
|
277
|
+
// Pattern 3: Unwrap with default
|
|
278
|
+
const { data: user } = unwrapOr(await client.get<User>('/users/1'), {
|
|
279
|
+
data: defaultUser,
|
|
280
|
+
status: 0,
|
|
281
|
+
headers: new Headers(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Pattern 4: matchError for specific error handling
|
|
285
|
+
if (isErr(result)) {
|
|
286
|
+
matchError(result.error, {
|
|
287
|
+
NotFound: (err) => console.error('User not found'),
|
|
288
|
+
RateLimit: (err) => console.error('Rate limited, slow down'),
|
|
289
|
+
Unauthorized: (err) => console.error('Need to re-authenticate'),
|
|
290
|
+
_: (err) => console.error('Unexpected error:', err.message),
|
|
291
|
+
});
|
|
270
292
|
}
|
|
271
293
|
```
|
|
272
294
|
|
|
273
|
-
|
|
295
|
+
**Available error classes:**
|
|
296
|
+
|
|
297
|
+
| Class | Status | Description |
|
|
298
|
+
| -------------------------- | ------ | ---------------------------- |
|
|
299
|
+
| `BadRequestError` | 400 | Invalid request |
|
|
300
|
+
| `UnauthorizedError` | 401 | Authentication required |
|
|
301
|
+
| `ForbiddenError` | 403 | Insufficient permissions |
|
|
302
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
303
|
+
| `ConflictError` | 409 | Resource conflict |
|
|
304
|
+
| `GoneError` | 410 | Resource no longer available |
|
|
305
|
+
| `UnprocessableEntityError` | 422 | Validation failed |
|
|
306
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
307
|
+
| `InternalServerError` | 500 | Server error |
|
|
308
|
+
| `ServiceUnavailableError` | 503 | Service down |
|
|
309
|
+
|
|
310
|
+
All error classes extend `FetchError`:
|
|
274
311
|
|
|
275
312
|
```typescript
|
|
276
313
|
class FetchError extends Error {
|
|
277
|
-
status: number;
|
|
278
|
-
|
|
279
|
-
body?: unknown; // Parsed response body (if available)
|
|
314
|
+
readonly status: number;
|
|
315
|
+
readonly body?: unknown; // Parsed response body (if available)
|
|
280
316
|
}
|
|
281
317
|
```
|
|
282
318
|
|
|
@@ -372,7 +408,7 @@ for await (const event of client.requestStream({
|
|
|
372
408
|
Use `@vertz/schema` for runtime validation of request/response data:
|
|
373
409
|
|
|
374
410
|
```typescript
|
|
375
|
-
import { FetchClient } from '@vertz/fetch';
|
|
411
|
+
import { FetchClient, isOk, unwrap } from '@vertz/fetch';
|
|
376
412
|
import { s } from '@vertz/schema';
|
|
377
413
|
|
|
378
414
|
// Define schemas
|
|
@@ -390,22 +426,26 @@ const client = new FetchClient({
|
|
|
390
426
|
// Validate responses in development
|
|
391
427
|
if (process.env.NODE_ENV === 'development') {
|
|
392
428
|
const data = await response.clone().json();
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
console.error('Response validation failed:', error);
|
|
429
|
+
const parsed = UserSchema.safeParse(data);
|
|
430
|
+
if (!parsed.success) {
|
|
431
|
+
console.error('Response validation failed:', parsed.error);
|
|
397
432
|
}
|
|
398
433
|
}
|
|
399
434
|
},
|
|
400
435
|
},
|
|
401
436
|
});
|
|
402
437
|
|
|
403
|
-
// Type-safe request with
|
|
404
|
-
const
|
|
438
|
+
// Type-safe request with Result handling
|
|
439
|
+
const result = await client.get<typeof UserSchema._output>('/users/1');
|
|
440
|
+
|
|
441
|
+
if (isOk(result)) {
|
|
442
|
+
const user = result.data.data; // Typed as UserSchema output
|
|
443
|
+
console.log(user.name);
|
|
444
|
+
}
|
|
405
445
|
|
|
406
|
-
// Or validate explicitly
|
|
407
|
-
const data = await client.
|
|
408
|
-
const user = UserSchema.parse(data
|
|
446
|
+
// Or unwrap and validate explicitly
|
|
447
|
+
const { data } = unwrap(await client.get<unknown>('/users/1'));
|
|
448
|
+
const user = UserSchema.parse(data); // Throws if invalid
|
|
409
449
|
```
|
|
410
450
|
|
|
411
451
|
## Advanced Examples
|
|
@@ -413,19 +453,23 @@ const user = UserSchema.parse(data.data); // Throws if invalid
|
|
|
413
453
|
### Timeout and Cancellation
|
|
414
454
|
|
|
415
455
|
```typescript
|
|
416
|
-
|
|
456
|
+
// Built-in timeout via config
|
|
457
|
+
const client = new FetchClient({
|
|
458
|
+
baseURL: 'https://api.example.com',
|
|
459
|
+
timeoutMs: 5000, // 5 second timeout for all requests
|
|
460
|
+
});
|
|
417
461
|
|
|
418
|
-
//
|
|
462
|
+
// Per-request cancellation with AbortController
|
|
463
|
+
const controller = new AbortController();
|
|
419
464
|
setTimeout(() => controller.abort(), 3000);
|
|
420
465
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
466
|
+
const result = await client.get('/slow-endpoint', {
|
|
467
|
+
signal: controller.signal,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (isErr(result)) {
|
|
471
|
+
// FetchTimeoutError for timeouts, FetchNetworkError for aborts
|
|
472
|
+
console.error(result.error.message);
|
|
429
473
|
}
|
|
430
474
|
```
|
|
431
475
|
|
|
@@ -464,11 +508,12 @@ const client = new FetchClient({
|
|
|
464
508
|
|
|
465
509
|
1. **Reuse client instances** — Create one client per base URL, not per request
|
|
466
510
|
2. **Use typed responses** — Always specify the response type for better IDE support
|
|
467
|
-
3. **Handle
|
|
468
|
-
4. **
|
|
469
|
-
5. **
|
|
470
|
-
6. **
|
|
471
|
-
7. **
|
|
511
|
+
3. **Handle results explicitly** — Check `isOk()`/`isErr()` instead of try/catch
|
|
512
|
+
4. **Use `matchError` for branching** — Exhaustive error handling with pattern matching
|
|
513
|
+
5. **Configure retries wisely** — Use exponential backoff for transient failures
|
|
514
|
+
6. **Add request logging in development** — Use `beforeRequest` hook for debugging
|
|
515
|
+
7. **Validate responses in development** — Use `@vertz/schema` + `afterResponse` hook
|
|
516
|
+
8. **Use streaming for large responses** — `requestStream` is more memory-efficient
|
|
472
517
|
|
|
473
518
|
## License
|
|
474
519
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/fetch",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
4
4
|
"description": "Type-safe HTTP client for Vertz",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@vertz/errors": "^0.2.
|
|
34
|
+
"@vertz/errors": "^0.2.41"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.3.1",
|