@vertz/fetch 0.2.42 → 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.
Files changed (2) hide show
  1. package/README.md +119 -74
  2. package/package.json +1 -1
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
- - **Error handling** — Typed error classes for all HTTP status codes
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 response = await client.request<{ id: number; name: string }>('GET', '/users/1');
37
- console.log(response.data.name); // Fully typed!
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.request<{ id: number }>('POST', '/users', {
41
- body: { name: 'Alice', email: 'alice@example.com' },
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.request<{ users: Array<{ id: number }> }>('GET', '/users', {
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 response = await client.request<User>('GET', '/users/1');
114
- const { data, status, headers } = response;
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
- ```typescript
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 non-2xx responses throw typed error classes:
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
- BadRequestError,
248
- UnauthorizedError,
249
- ForbiddenError,
250
- NotFoundError,
251
- ConflictError,
252
- GoneError,
253
- UnprocessableEntityError,
254
- RateLimitError,
255
- InternalServerError,
256
- ServiceUnavailableError,
257
- FetchError, // Base class
258
- } from '@vertz/fetch';
259
-
260
- try {
261
- await client.request('GET', '/users/999');
262
- } catch (error) {
263
- if (error instanceof NotFoundError) {
264
- console.error('User not found:', error.statusText);
265
- console.error('Response body:', error.body);
266
- } else if (error instanceof RateLimitError) {
267
- console.error('Rate limited, retry after:', error.statusText);
268
- }
269
- throw error;
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
- All error classes extend `FetchError` with these properties:
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
- statusText: string;
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
- try {
394
- UserSchema.parse(data);
395
- } catch (error) {
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 schema validation
404
- const response = await client.request<typeof UserSchema._output>('GET', '/users/1');
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.request<unknown>('GET', '/users/1');
408
- const user = UserSchema.parse(data.data); // Throws if invalid
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
- const controller = new AbortController();
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
- // Cancel after 3 seconds
462
+ // Per-request cancellation with AbortController
463
+ const controller = new AbortController();
419
464
  setTimeout(() => controller.abort(), 3000);
420
465
 
421
- try {
422
- const response = await client.request('GET', '/slow-endpoint', {
423
- signal: controller.signal,
424
- });
425
- } catch (error) {
426
- if (error.name === 'AbortError') {
427
- console.error('Request cancelled');
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 errors explicitly** — Catch specific error classes for better error handling
468
- 4. **Configure retries wisely** — Use exponential backoff for transient failures
469
- 5. **Add request logging in development** — Use `beforeRequest` hook for debugging
470
- 6. **Validate responses in development** — Use `@vertz/schema` + `afterResponse` hook
471
- 7. **Use streaming for large responses** — `requestStream` is more memory-efficient
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.42",
3
+ "version": "0.2.43",
4
4
  "description": "Type-safe HTTP client for Vertz",
5
5
  "license": "MIT",
6
6
  "repository": {