@pureq/pureq 1.1.5 → 1.1.6

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 +47 -1125
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,1166 +1,88 @@
1
1
  # pureq
2
2
 
3
- Functional, immutable, type-safe HTTP transport layer for TypeScript.
3
+ ## Functional, immutable, and type-safe HTTP transport layer for TypeScript
4
4
 
5
- [Quick Start](#quick-start) · [Why pureq](#why-pureq) · [Middleware](#reliability-middleware) · [React Query](#react-query--swr-integration) · [BFF / Backend](#bff--backend-patterns) · [API Reference](#api-reference)
5
+ [Get Started](./docs/getting_started.md) | [Documentation](./docs/README.md) | [Middleware Reference](./docs/middleware_reference.md) | [GitHub](https://github.com/shiro-shihi/pureq)
6
6
 
7
7
  ---
8
8
 
9
- **pureq** is not another fetch wrapper. It's a **policy-first transport layer** that makes HTTP behavior explicit, composable, and observable across frontend, BFF, backend, and edge runtimes.
9
+ pureq is a policy-first transport layer that makes HTTP behavior explicit, composable, and observable across frontend, BFF, backend, and edge runtimes. It is designed to replace ad-hoc fetch wrappers with a robust, immutable system for managing engineering-grade reliability.
10
10
 
11
11
  ```ts
12
12
  import { createClient, retry, circuitBreaker, dedupe } from "pureq";
13
13
 
14
14
  const api = createClient({ baseURL: "https://api.example.com" })
15
- .use(dedupe())
16
- .use(retry({ maxRetries: 2, delay: 200 }))
17
- .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 }));
18
-
19
- const user = await api.getJson<User>("/users/:id", { params: { id: "42" } });
20
- ```
21
-
22
- Zero runtime dependencies in the core package. Works everywhere `fetch` works.
23
-
24
- ## Install
25
-
26
- ```bash
27
- npm install @pureq/pureq
28
- ```
29
-
30
- ### Node.js (with FileSystem support)
31
-
32
- If using Node.js specific adapters (like `FileSystemQueueStorageAdapter`), use the node subpath:
33
-
34
- ```ts
35
- import { FileSystemQueueStorageAdapter } from "pureq/node";
36
- ```
37
-
38
- > [!NOTE]
39
- > Core package (`pureq`) is zero-dependency and runtime-agnostic.
40
- > Node-only adapters (for example `FileSystemQueueStorageAdapter`) live behind `pureq/node` so browser bundles stay clean and small.
41
-
42
- ## Quick Start
43
-
44
- ### A quicker start than raw fetch
45
-
46
- ```ts
47
- import { createClient } from "pureq";
48
-
49
- type User = { id: string; name: string };
50
-
51
- const client = createClient({ baseURL: "https://api.example.com" });
52
-
53
- // GET + status check + JSON parse + typed path params
54
- const user = await client.getJson<User>("/users/:userId", {
55
- params: { userId: "42" },
56
- });
57
-
58
- console.log(user.name);
59
- ```
60
-
61
- ### Type-safe path parameters (Template Literal Types)
62
-
63
- ```ts
64
- // Param keys are derived from the URL template at compile time.
65
-
66
- // ✅ Compiles
67
- await client.get("/users/:userId/posts/:postId", {
68
- params: { userId: "1", postId: "42" },
69
- });
70
-
71
- // ❌ TypeScript error: missing postId
72
- await client.get("/users/:userId/posts/:postId", {
73
- params: { userId: "1" },
74
- });
75
- ```
76
-
77
- ### Result API to prevent missed error handling
78
-
79
- ```ts
80
- const result = await client.getJsonResult<User>("/users/:userId", {
81
- params: { userId: "42" },
82
- });
83
-
84
- if (!result.ok) {
85
- // Switching on kind gives explicit, exhaustive transport handling.
86
- switch (result.error.kind) {
87
- case "network":
88
- showOfflineBanner();
89
- break;
90
- case "timeout":
91
- showRetryToast();
92
- break;
93
- case "http":
94
- if (result.error.status === 401) redirectToLogin();
95
- break;
96
- case "circuit-open":
97
- switchToFallbackMode();
98
- break;
99
- case "aborted":
100
- case "storage-error":
101
- case "auth-error":
102
- case "validation-error":
103
- case "unknown":
104
- reportTransportError(result.error);
105
- break;
106
- default: {
107
- const exhaustive: never = result.error.kind;
108
- throw new Error(`Unhandled error kind: ${exhaustive}`);
109
- }
110
- }
111
- return;
112
- }
113
-
114
- // result.data is strongly typed JSON
115
- renderUser(result.data);
116
- ```
117
-
118
- Every request method has a `*Result` variant that never throws — transport failures become values you can pattern-match on.
119
-
120
- ### Systematic Error Codes
121
-
122
- For enterprise-grade observability, `PureqError` exposes both `kind` and `code` on the same error object. They represent the same category in two formats:
123
-
124
- - `kind`: human-friendly lowercase string (e.g. `"timeout"`)
125
- - `code`: machine-readable Screaming Snake Case (e.g. `"PUREQ_TIMEOUT"`)
126
-
127
- ```ts
128
- if (!result.ok) {
129
- if (result.error.kind === "timeout" && result.error.code === "PUREQ_TIMEOUT") {
130
- // Handle specifically...
131
- }
132
- }
133
- ```
134
-
135
- Common codes: `PUREQ_TIMEOUT`, `PUREQ_NETWORK_ERROR`, `PUREQ_OFFLINE_QUEUE_FULL`, `PUREQ_AUTH_REFRESH_FAILED`, `PUREQ_VALIDATION_ERROR`.
136
-
137
- ---
138
-
139
- ## Why pureq
140
-
141
- ### The problem with raw `fetch`
142
-
143
- `fetch` is a primitive. It gives you a single request/response cycle and nothing else. Every team ends up rebuilding the same things on top of it:
144
-
145
- ```ts
146
- // This is what real-world fetch code looks like
147
- async function fetchUser(id: string) {
148
- const controller = new AbortController();
149
- const timeout = setTimeout(() => controller.abort(), 5000);
150
-
151
- try {
152
- const response = await fetch(`/api/users/${id}`, {
153
- signal: controller.signal,
154
- });
155
- clearTimeout(timeout);
156
-
157
- if (response.status === 429) {
158
- // retry? how many times? what delay?
159
- }
160
- if (response.status >= 500) {
161
- // retry? circuit break? log?
162
- }
163
- if (!response.ok) {
164
- throw new Error(`HTTP ${response.status}`);
165
- }
166
- return await response.json();
167
- } catch (err) {
168
- clearTimeout(timeout);
169
- // is it a timeout? network error? abort? how do we tell?
170
- throw err;
171
- }
172
- }
173
- ```
174
-
175
- Every endpoint handler re-decides retry, timeout, error shape, and observability. There's no consistency, no composition, and no governance.
176
-
177
- **pureq replaces that with:**
178
-
179
- ```ts
180
- const api = createClient({ baseURL: "/api" })
181
- .use(retry({ maxRetries: 2, delay: 200, retryOnStatus: [429, 500, 502, 503] }))
182
- .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 }));
183
-
184
- const user = await api.getJson<User>("/users/:id", {
185
- params: { id },
186
- timeout: 5000,
187
- });
188
- ```
189
-
190
- Policy is declared once, applied everywhere, and enforced by the type system.
191
-
192
- ### How pureq compares to axios
193
-
194
- axios is familiar and battle-tested. But its mutability model makes transport behavior hard to reason about at scale:
195
-
196
- | Concern | **axios** | **pureq** |
197
- | --- | --- | --- |
198
- | Client model | Mutable instances | Immutable — `.use()` returns new client |
199
- | Retry/Circuit breaker | External packages (axios-retry, etc.) | Built-in middleware |
200
- | Error model | Throws by default, boolean flags | `Result<T, E>` pattern — no exceptions |
201
- | Path params | String interpolation | Type-safe `:param` templates |
202
- | Middleware model | Interceptors (mutate config) | Onion middleware (compose behavior) |
203
- | Policy guardrails | None | Validates invalid combinations at startup |
204
- | Observability | Interceptor-based logging | Structured event hooks + OTel export |
205
- | Bundle | ~14 KB gzipped + adapters | Zero-dependency, tree-shakeable |
206
-
207
- pureq isn't "better" than axios universally. But if you want **explicit transport policy** that doesn't drift across a growing codebase, pureq is designed for that.
208
-
209
- ### pureq vs React Query / SWR
210
-
211
- pureq does **not** replace React Query or SWR. They solve different problems:
212
-
213
- | Concern | **React Query / SWR** | **pureq** |
214
- | --- | --- | --- |
215
- | Cache lifecycle | ✅ stale-while-revalidate, GC, refetch | ❌ not a UI cache |
216
- | Query keys | ✅ declarative caching | ❌ |
217
- | Suspense integration | ✅ | ❌ |
218
- | UI state (loading/error) | ✅ | ❌ |
219
- | Retry + backoff | ⚠️ basic | ✅ full control |
220
- | Circuit breaker | ❌ | ✅ |
221
- | Deadline propagation | ❌ | ✅ |
222
- | Request dedup | ⚠️ by query key | ✅ by request signature |
223
- | Concurrency limits | ❌ | ✅ |
224
- | Hedged requests | ❌ | ✅ |
225
- | Offline queue | ❌ | ✅ |
226
- | Request observability | ❌ | ✅ OTel-ready |
227
- | Idempotency keys | ❌ | ✅ |
228
- | Multi-runtime | ⚠️ React-only | ✅ Any JS runtime |
229
-
230
- **They compose perfectly together** — pureq is the transport layer *underneath* React Query:
231
-
232
- ```ts
233
- import { useQuery } from "@tanstack/react-query";
234
- import { createClient, retry, circuitBreaker } from "pureq";
235
-
236
- const api = createClient({ baseURL: "https://api.example.com" })
237
- .use(retry({ maxRetries: 2, delay: 200 }))
238
- .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 }));
239
-
240
- function useUser(id: string) {
241
- return useQuery({
242
- queryKey: ["user", id],
243
- queryFn: () => api.getJson<User>("/users/:id", { params: { id } }),
244
- });
245
- }
246
- ```
247
-
248
- React Query handles: cache lifecycle, stale-while-revalidate, refetching, and suspense.
249
- pureq handles: retry strategy, circuit breaking, timeouts, dedup, and telemetry.
250
-
251
- Clean separation. No overlap.
252
-
253
- ---
254
-
255
- ## Core Concepts
256
-
257
- ### Immutable client composition
258
-
259
- Every call to `.use()`, `.useRequestInterceptor()`, or `.useResponseInterceptor()` returns a **new** client instance. The original is never mutated.
260
-
261
- ```ts
262
- const base = createClient({ baseURL: "https://api.example.com" });
263
- const withRetry = base.use(retry({ maxRetries: 2, delay: 200 }));
264
- const withAuth = withRetry.useRequestInterceptor((req) => ({
265
- ...req,
266
- headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
267
- }));
268
-
269
- // base, withRetry, and withAuth are three separate clients
270
- ```
271
-
272
- This makes it trivial to share a base client while customizing per-dependency behavior.
273
-
274
- ### The "Why" of Immutability
275
-
276
- Unlike mutable interceptor stacks, pureq clients are branchable snapshots.
277
-
278
- ```ts
279
- import { authRefresh, createClient, dedupe, retry } from "pureq";
280
-
281
- const base = createClient({ baseURL: "https://api.example.com" })
282
- .use(retry({ maxRetries: 2, delay: 200 }));
283
-
284
- // Public API: cache-friendly and lightweight
285
- const publicApi = base.use(dedupe({ methods: ["GET", "HEAD"] }));
286
-
287
- // Auth API: isolated auth policy without mutating publicApi
288
- const authedApi = base
289
- .use(authRefresh({ refresh: refreshToken }))
290
- .useRequestInterceptor((req) => ({
291
- ...req,
292
- headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
293
- }));
294
- ```
295
-
296
- This pattern is especially useful in BFFs: you keep shared defaults, then safely branch per upstream or trust boundary.
297
-
298
- ### Middleware: the Onion Model
299
-
300
- Middleware wraps the entire request lifecycle. Each middleware can intercept the request before it happens, await the result, and post-process the response:
301
-
302
- ```text
303
- Request
304
- -> [dedupe]
305
- -> [retry]
306
- -> [circuitBreaker]
307
- -> [adapter/fetch]
308
- <- [circuitBreaker]
309
- <- [retry]
310
- <- [dedupe]
311
- Response
312
- ```
313
-
314
- Think of `retry` and `circuitBreaker` as wrappers around `next()`.
315
-
316
- - Outer wrappers see the whole execution envelope.
317
- - Inner wrappers only see what reaches them.
318
-
319
- That is why order changes behavior. Example:
320
-
321
- - `retry` outside `circuitBreaker`: each retry attempt passes through breaker logic.
322
- - `circuitBreaker` outside `retry`: breaker sees one wrapped operation that may include internal retries.
323
-
324
- Middleware can:
325
-
326
- - Transform the request before `next()`
327
- - Decide whether to call `next()` at all (e.g., cache hit, circuit open)
328
- - Retry `next()` on failure (e.g., retry middleware)
329
- - Transform the response after `next()`
330
-
331
- ### Interceptors vs Middleware
332
-
333
- pureq distinctly separates lifecycle control from simple data transformation:
334
-
335
- - **Middleware (Onion Model)**: Used for async control flow. Middleware wrappers can pause, retry, hedge, or completely short-circuit the network request (e.g., caching). They govern the *lifetime* and state of the request.
336
- - **Interceptors**: Used for pure data transformation. `useRequestInterceptor` and `useResponseInterceptor` are lightweight hooks to modify the shape of the request or response (e.g., synchronously adding a token header) without the boilerplate of managing async state or the `next()` function cascade.
337
-
338
- ### Type-safe path parameters
339
-
340
- Route templates like `/users/:userId/posts/:postId` are type-checked at compile time:
341
-
342
- ```ts
343
- // ✅ Compiles — params match the URL template
344
- await client.get("/users/:userId/posts/:postId", {
345
- params: { userId: "1", postId: "42" },
346
- });
347
-
348
- // ❌ TypeScript error — missing 'postId'
349
- await client.get("/users/:userId/posts/:postId", {
350
- params: { userId: "1" },
351
- });
352
- ```
353
-
354
- ### Result-based error handling
355
-
356
- Instead of `try/catch` everywhere, use the `*Result` variants for explicit error handling:
357
-
358
- ```ts
359
- const result = await client.postResult("/orders", orderData);
360
-
361
- if (!result.ok) {
362
- switch (result.error.kind) {
363
- case "network":
364
- showOfflineNotice();
365
- break;
366
- case "timeout":
367
- showRetryPrompt();
368
- break;
369
- case "circuit-open":
370
- showDegradedMode();
371
- break;
372
- default:
373
- logError(result.error);
374
- }
375
- return;
376
- }
377
-
378
- // result.data is HttpResponse
379
- const order = await result.data.json<Order>();
380
- ```
381
-
382
- ### Streams and Binary Data
383
-
384
- When reading payloads via `.arrayBuffer()`, `.blob()`, or `response.body` (`ReadableStream`), standard `fetch` semantics dictate that the initial Promise resolves as soon as HTTP headers are received.
385
-
386
- pureq inherently protects the *entire stream lifecycle*. Middleware policies like `deadline()` or `defaultTimeout()` bind an `AbortSignal` to the underlying `fetch`. If a timeout is exceeded while actively downloading the body stream off the network, the signal propagates down and automatically aborts the stream, preventing memory and resource leaks safely.
387
-
388
- ---
389
-
390
- ## Reliability Middleware
391
-
392
- All middleware is composable. Stack them in the order you want:
393
-
394
- ```ts
395
- const client = createClient({ baseURL: "https://api.example.com" })
396
15
  .use(dedupe()) // collapse duplicate in-flight GETs
397
- .use(httpCache({ ttlMs: 10_000, maxEntries: 200 })) // in-memory cache with LRU eviction
398
- .use(retry({ maxRetries: 3, delay: 200, backoff: true })) // exponential backoff
399
- .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 }))
400
- .use(validation({ validate: (data) => !!data })) // zero-dependency schema validation
401
- .use(fallback({ value: { body: "default", status: 200 } as any })); // graceful degradation
402
- ```
403
-
404
- ### Standard Order (recommended)
405
-
406
- For most production stacks, this order is a safe default:
407
-
408
- ```text
409
- [dedupe] -> [httpCache] -> [retry] -> [circuitBreaker] -> [validation] -> [adapter]
410
- ```
411
-
412
- Why this order:
413
-
414
- - `dedupe` outermost to collapse duplicate in-flight work early.
415
- - `httpCache` before retry to short-circuit cache hits.
416
- - `retry` before `circuitBreaker` so each attempt is still guarded by breaker state.
417
- - `validation` near the inner edge to validate final payloads before returning to callers.
418
-
419
- If order is reversed accidentally, behavior can drift (for example, retrying inside a layer you expected to run once).
420
-
421
- ### Retry
422
-
423
- Full-featured retry with exponential backoff, Retry-After header respect, and retry budget.
424
-
425
- ```ts
426
- import { retry } from "pureq";
427
-
428
- client.use(retry({
429
- maxRetries: 3,
430
- delay: 200,
431
- methods: ["GET", "PUT", "DELETE"], // Safe defaults: idempotent methods only
432
- backoff: true,
433
- onRetry: ({ attempt, error }) => console.warn(`Retry #${attempt}`, error),
434
- }));
435
- ```
436
-
437
- **Safety First**: By default, `retry` only targets idempotent methods. To retry `POST` or `PATCH`, you must explicitly add them to `methods` and ensure the backend supports idempotency keys.
438
-
439
- ```text
440
- POST /api/orders HTTP/1.1
441
- Idempotency-Key: abc-123
442
- ```
443
-
444
- ### Deadline Propagation
445
-
446
- Enforces a total request budget across all retry attempts — different from a per-request timeout.
447
-
448
- ```ts
449
- import { deadline, retry } from "pureq";
450
-
451
- const client = createClient()
452
- .use(deadline({ defaultTimeoutMs: 1500 })) // 1.5s total, no matter how many retries
453
- .use(retry({ maxRetries: 3, delay: 200 }));
454
- ```
455
-
456
- ### Circuit Breaker
457
-
458
- Stops sending requests to a failing dependency. Automatically probes for recovery.
459
-
460
- ```ts
461
- import { circuitBreaker } from "pureq";
462
-
463
- client.use(circuitBreaker({
464
- failureThreshold: 5, // open after 5 consecutive failures
465
- successThreshold: 2, // close after 2 successes in half-open
466
- cooldownMs: 30_000, // probe again after 30s
467
- }));
468
- ```
469
-
470
- ### Concurrency Limit
471
-
472
- Caps in-flight requests globally or by key to protect backend resources.
473
-
474
- ```ts
475
- import { concurrencyLimit } from "pureq";
476
-
477
- client.use(concurrencyLimit({
478
- maxConcurrent: 20,
479
- keyBuilder: (req) => new URL(req.url).hostname,
480
- }));
481
- ```
482
-
483
- ### Hedged Requests
484
-
485
- Issues a duplicate request after a short delay for tail-latency-sensitive reads. The first response wins; the loser is aborted.
486
-
487
- ```ts
488
- import { hedge } from "pureq";
489
-
490
- client.use(hedge({
491
- hedgeAfterMs: 80,
492
- methods: ["GET"],
493
- }));
494
- ```
495
-
496
- ### Request Deduplication
497
-
498
- Collapses concurrent duplicate GET requests into a single in-flight call.
499
-
500
- ```ts
501
- import { dedupe } from "pureq";
502
-
503
- client.use(dedupe({
504
- methods: ["GET", "HEAD"],
505
- }));
506
- ```
507
-
508
- ### HTTP Cache
509
-
510
- In-memory cache with ETag revalidation and stale-if-error fallback.
511
-
512
- ```ts
513
- import { httpCache } from "pureq";
514
-
515
- client.use(httpCache({
516
- ttlMs: 10_000,
517
- staleIfErrorMs: 60_000,
518
- maxEntries: 500, // LRU eviction when full
519
- }));
520
- ```
521
-
522
- ### Offline Queue
523
-
524
- Queues mutation requests when offline and replays them when connectivity restores.
525
-
526
- ```ts
527
- import { createOfflineQueue, idempotencyKey } from "pureq";
528
-
529
- // 1. Create durable storage (IndexedDB for browser, FS for Node)
530
- const storage = new IndexedDBQueueStorageAdapter();
16
+ .use(retry({ maxRetries: 2, delay: 200 })) // exponential backoff
17
+ .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 })); // stop on outages
531
18
 
532
- // 2. (Optional) Wrap with encryption for enterprise security
533
- // myCryptoKey can come from crypto.subtle.generateKey(...) or importKey/deriveKey from a password.
534
- const encryptedStorage = new EncryptedQueueStorageAdapter(storage, myCryptoKey);
535
-
536
- const queue = createOfflineQueue({
537
- storage: encryptedStorage,
538
- methods: ["POST", "PUT", "PATCH"],
539
- ttlMs: 24 * 60 * 60 * 1000, // 24h expiration
540
- lockName: "my-app-offline-lock", // Multi-tab coordination
541
- });
542
-
543
- const client = createClient()
544
- .use(idempotencyKey())
545
- .use(queue.middleware);
546
-
547
- // Later, when back online:
548
- await queue.flush((req) => client.post(req.url, req.body));
549
- ```
550
-
551
- **Durable Adapters**:
552
-
553
- - `IndexedDBQueueStorageAdapter`: Standard browser persistence.
554
- - `FileSystemQueueStorageAdapter`: Node.js persistence (import from `pureq/node`).
555
- - `EncryptedQueueStorageAdapter`: Wrapper that encrypts data at rest using AES-GCM.
556
-
557
- ```ts
558
- // Example: Creating an encrypted storage
559
- const encrypted = new EncryptedQueueStorageAdapter(new IndexedDBQueueStorageAdapter(), key);
560
- ```
561
-
562
- ### Idempotency Keys
563
-
564
- Automatically injects idempotency key headers for mutation requests.
565
-
566
- ```ts
567
- import { idempotencyKey } from "pureq";
568
-
569
- client.use(idempotencyKey({
570
- headerName: "Idempotency-Key", // default
571
- methods: ["POST", "PUT", "PATCH", "DELETE"],
572
- }));
573
- ```
574
-
575
- ### Auth Refresh
576
-
577
- Automatically handles 401 Unauthorized errors by refreshing the token and retrying the request. Includes built-in "thundering herd" prevention to ensure only one refresh request is in-flight at a time.
578
-
579
- ```ts
580
- import { authRefresh } from "pureq";
581
-
582
- client.use(authRefresh({
583
- refresh: async () => {
584
- // Multiple 401s will only trigger ONE refresh call.
585
- const res = await fetch("/api/refresh", { method: "POST" });
586
- return (await res.json()).token;
587
- },
588
- // Optional: customize how the request is updated
589
- updateRequest: (req, token) => ({
590
- ...req,
591
- headers: { ...req.headers, Authorization: `Bearer ${token}` }
592
- })
593
- }));
594
- ```
595
-
596
- ### Validation
597
-
598
- A zero-dependency bridge to any schema validation library (Zod, Valibot, etc.) or custom type guards.
599
-
600
- ```ts
601
- import { validation } from "pureq";
602
- import { z } from "zod";
603
-
604
- const UserSchema = z.object({ id: z.string(), name: z.string() });
605
-
606
- client.use(validation({
607
- validate: (data) => UserSchema.parse(data), // Throws PUREQ_VALIDATION_ERROR on failure
608
- message: "Invalid API response schema"
609
- }));
19
+ // GET + status check + JSON parse + typed path params
20
+ const user = await api.getJson<User>("/users/:id", { params: { id: "42" } });
610
21
  ```
611
22
 
612
- ### Fallback
23
+ ## Philosophy
613
24
 
614
- Enables "Graceful Degradation" by returning a default value or cached data when a request fails.
25
+ The core philosophy of pureq is that **transport is a policy**. Communication between services should not be hidden behind imperative logic or mutable configurations. Instead, pureq treats reliability, observability, and security as first-class citizens that are composed as immutable layers.
615
26
 
616
- ```ts
617
- import { fallback, HttpResponse, type PureqError } from "pureq";
618
-
619
- const isPureqError = (value: unknown): value is PureqError => {
620
- return typeof value === "object" && value !== null && "code" in value && "kind" in value;
621
- };
27
+ - **Immutability as Safety**: Every change to a client returns a new instance, preventing side-effects across shared infrastructure.
28
+ - **Failures as Values**: Through the Result pattern, errors are treated as data to be handled explicitly, not exceptions that disrupt the flow.
29
+ - **Policy over Code**: Reliability logic (retries, timeouts, breakers) is declared as a policy stack, separated from the business logic of individual requests.
622
30
 
623
- client.use(fallback({
624
- value: new HttpResponse(new Response(JSON.stringify({ items: [] }), { status: 200 })),
625
- when: (trigger) =>
626
- trigger.type === "error" &&
627
- isPureqError(trigger.error) &&
628
- trigger.error.code === "PUREQ_TIMEOUT" &&
629
- trigger.error.kind === "timeout"
630
- }));
631
- ```
31
+ ## Key Features
632
32
 
633
- ### Policy Guardrails
33
+ - **Immutable Client Composition**: Safely branch and share transport configurations without mutation leaks.
34
+ - **Onion Model Middleware**: Powerful, composable async lifecycle control for retries, caching, and circuit breaking.
35
+ - **Strictly Type-Safe**: Compile-time validation for URL path parameters and response schema structures.
36
+ - **Non-throwing API**: Native support for the Result pattern to ensure exhaustive error handling.
37
+ - **Zero Runtime Dependencies**: Ultra-lightweight core that works in any JS environment (Node, Browser, Bun, Edge).
38
+ - **Enterprise Observability**: Built-in policy tracing, performance metrics, and OpenTelemetry mapping.
634
39
 
635
- pureq validates your middleware stack at client creation time and rejects invalid combinations:
636
-
637
- ```ts
638
- // ❌ Throws: "pureq: multiple retry policies are not allowed in one client"
639
- createClient()
640
- .use(retry({ maxRetries: 2, delay: 200 }))
641
- .use(retry({ maxRetries: 3, delay: 300 }));
642
-
643
- // ❌ Throws: "pureq: use deadline or defaultTimeout, not both"
644
- createClient()
645
- .use(deadline({ defaultTimeoutMs: 1500 }))
646
- .use(defaultTimeout(3000));
647
- ```
648
-
649
- ---
40
+ ## Documentation Index
650
41
 
651
- ## Presets
42
+ | Guide | Description |
43
+ | --- | --- |
44
+ | [Getting Started](./docs/getting_started.md) | Installation and your first request |
45
+ | [Core Concepts](./docs/core_concepts.md) | Immutability, the Onion Model, and Path Typing |
46
+ | [Middleware Reference](./docs/middleware_reference.md) | Detailed guide for all reliability policies |
47
+ | [Error Handling](./docs/error_handling.md) | The Result pattern and Error codes reference |
48
+ | [Observability](./docs/observability.md) | Lifecycle hooks, metrics, and OTel integration |
49
+ | [Integrations](./docs/README.md#integrations) | React Query, SWR, and Backend patterns |
652
50
 
653
- For teams that want production-ready defaults without configuring each middleware:
51
+ ## Installation
654
52
 
655
- ```ts
656
- import { createClient, frontendPreset, bffPreset, backendPreset } from "pureq";
657
-
658
- // Frontend: conservative retries, short timeout, dedup for GETs
659
- let frontend = createClient();
660
- for (const mw of frontendPreset()) frontend = frontend.use(mw);
661
-
662
- // BFF: balanced latency vs stability, idempotency for mutations
663
- let bff = createClient();
664
- for (const mw of bffPreset()) bff = bff.use(mw);
665
-
666
- // Backend: aggressive retries, circuit breaker, no dedup
667
- let backend = createClient();
668
- for (const mw of backendPreset()) backend = backend.use(mw);
53
+ ```bash
54
+ npm install @pureq/pureq
669
55
  ```
670
56
 
671
- | Preset | Timeout | Retries | Dedup | Circuit Breaker | Idempotency |
672
- | ------ | ------- | ------- | ----- | --------------- | ----------- |
673
- | `frontendPreset()` | 5s | 1 | GET/HEAD | 4 failures / 10s cooldown | ✅ |
674
- | `bffPreset()` | 3s | 2 | GET/HEAD | 5 failures / 20s cooldown | ✅ body-only |
675
- | `backendPreset()` | 2.5s | 3 | off | 6 failures / 30s cooldown | ✅ body-only |
676
- | `resilientPreset()` | none (pair with `defaultTimeout()` if needed) | 2 | GET/HEAD | 5 failures / 30s cooldown | ✅ |
677
-
678
- All presets are built from the same public middleware. You can inspect and override any parameter.
679
-
680
- ---
681
-
682
- ## Observability
683
-
684
- ### Client lifecycle hooks
57
+ For Node.js specific adapters (FileSystem storage, etc.), use the node subpath:
685
58
 
686
59
  ```ts
687
- const client = createClient({
688
- hooks: {
689
- onRequestStart: (event) => {
690
- console.log(`→ ${event.method} ${event.url} [${event.requestId}]`);
691
- },
692
- onRequestSuccess: (event) => {
693
- console.log(`✓ ${event.status} in ${event.latencyMs}ms [retries: ${event.retryCount}]`);
694
- },
695
- onRequestError: (event) => {
696
- console.error(`✗ ${event.errorKind}: ${event.error.message}`);
697
- },
698
- },
699
- });
60
+ import { FileSystemQueueStorageAdapter } from "pureq/node";
700
61
  ```
701
62
 
702
- ### Diagnostics middleware
703
-
704
- Collect per-request performance metrics and policy traces:
63
+ ## Usage Example
705
64
 
706
65
  ```ts
707
- import { createMiddlewareDiagnostics, createConsoleDiagnosticsExporter } from "pureq";
66
+ import { createClient, frontendPreset } from "pureq";
708
67
 
709
- const diagnostics = createMiddlewareDiagnostics({
710
- onEvent: createConsoleDiagnosticsExporter().export,
68
+ // Initialize with production-ready defaults
69
+ const api = createClient({
70
+ baseURL: "https://api.example.com",
71
+ middlewares: frontendPreset(),
711
72
  });
712
73
 
713
- const client = createClient().use(diagnostics.middleware);
714
-
715
- // Inspect metrics
716
- const snap = diagnostics.snapshot();
717
- console.log(snap.p50, snap.p95, snap.total, snap.success, snap.failed);
718
- ```
719
-
720
- ### Policy Tracing (Debuggability)
721
-
722
- Ever wonder *why* a request was retried or why the circuit opened? `pureq` records a detailed decision trace for every request.
723
-
724
- ```ts
725
- import { explainPolicyTrace } from "pureq";
726
-
727
- try {
728
- await client.get("/flakey-endpoint");
729
- } catch (err) {
730
- // Prints exactly what happened:
731
- // [2026-04-10T10:00:00Z] RETRY: RETRY (status=503)
732
- // [2026-04-10T10:00:01Z] RETRY: RETRY (status=503)
733
- // [2026-04-10T10:00:02Z] CIRCUIT-BREAKER: TRIP (reason="failure threshold exceeded")
734
- console.log(explainPolicyTrace(err.request));
735
- }
736
- ```
737
-
738
- ### OpenTelemetry integration
739
-
740
- Map transport events to OTel-compatible attributes:
741
-
742
- ```ts
743
- import {
744
- mapToStandardHttpAttributes,
745
- mapToAwsSemanticConventions,
746
- mapToGcpSemanticConventions,
747
- } from "pureq";
748
-
749
- // Standard OTel HTTP semantic conventions
750
- const attrs = mapToStandardHttpAttributes(event);
751
- // { "http.method": "GET", "http.url": "...", "http.status_code": 200, ... }
752
- ```
753
-
754
- ### Redaction for safe telemetry
755
-
756
- Built-in redaction profiles prevent sensitive data from leaking into logs and telemetry:
757
-
758
- ```ts
759
- import {
760
- redactHeaders,
761
- redactObjectFields,
762
- redactUrlQueryParams,
763
- piiRedactionOptions,
764
- authRedactionOptions,
765
- } from "pureq";
766
-
767
- redactHeaders(headers);
768
- // Authorization: "[REDACTED]", Cookie: "[REDACTED]", ...
769
-
770
- redactUrlQueryParams("https://api.example.com/v1?token=secret123&page=1");
771
- // "https://api.example.com/v1?token=[REDACTED]&page=1"
772
-
773
- redactObjectFields(body, piiRedactionOptions);
774
- // { email: "[REDACTED]", phone: "[REDACTED]", name: "Alice" }
775
- ```
776
-
777
- ---
778
-
779
- ## React Query / SWR Integration
780
-
781
- ### React Query
782
-
783
- ```ts
784
- import { useQuery, useMutation } from "@tanstack/react-query";
785
- import { createClient, retry } from "pureq";
786
-
787
- const api = createClient({ baseURL: "https://api.example.com" })
788
- .use(retry({ maxRetries: 2, delay: 200 }));
789
-
790
- // Queries
791
- function useUser(id: string) {
792
- return useQuery({
793
- queryKey: ["user", id],
794
- queryFn: () => api.getJson<User>("/users/:id", { params: { id } }),
795
- });
796
- }
797
-
798
- // Mutations
799
- function useCreatePost() {
800
- return useMutation({
801
- mutationFn: (data: CreatePostInput) =>
802
- api.postJson<Post>("/posts", data),
803
- });
804
- }
805
- ```
806
-
807
- ### SWR
808
-
809
- ```ts
810
- import useSWR from "swr";
811
- import { createClient } from "pureq";
812
-
813
- const api = createClient({ baseURL: "https://api.example.com" });
814
-
815
- function useUser(id: string) {
816
- return useSWR(
817
- ["user", id],
818
- () => api.getJson<User>("/users/:id", { params: { id } }),
819
- );
820
- }
821
- ```
822
-
823
- **Separation of concerns:**
824
-
825
- - **React Query / SWR** → cache lifecycle, stale-while-revalidate, background refetch, suspense, UI state
826
- - **pureq** → retry, circuit breaking, timeout, dedup, concurrency, telemetry, error normalization
827
-
828
- You get the best of both worlds.
829
-
830
- ---
831
-
832
- ## BFF / Backend Patterns
833
-
834
- ### BFF (Backend-For-Frontend)
835
-
836
- A BFF aggregates multiple upstream APIs for the frontend. pureq gives you explicit per-dependency policy:
837
-
838
- ```ts
839
- // One client per upstream service — explicit, isolated policies
840
- const userService = createClient({ baseURL: "https://user-service.internal" })
841
- .use(retry({ maxRetries: 2, delay: 150 }))
842
- .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 20_000 }))
843
- .useRequestInterceptor((req) => ({
844
- ...req,
845
- headers: { ...req.headers, "X-Internal-Auth": getServiceToken() },
846
- }));
847
-
848
- const paymentService = createClient({ baseURL: "https://payment-service.internal" })
849
- .use(retry({ maxRetries: 1, delay: 500 }))
850
- .use(circuitBreaker({ failureThreshold: 3, cooldownMs: 60_000 }));
851
-
852
- // BFF handler
853
- async function handleGetUserProfile(userId: string) {
854
- const user = await userService.getJson<User>("/users/:id", {
855
- params: { id: userId },
856
- });
857
-
858
- const paymentResult = await paymentService.getResult("/payments/user/:id", {
859
- params: { id: userId },
860
- });
861
-
862
- return {
863
- ...user,
864
- payments: paymentResult.ok ? await paymentResult.data.json() : [],
865
- };
866
- }
867
- ```
868
-
869
- ### Backend service-to-service
870
-
871
- ```ts
872
- import { createClient, backendPreset } from "pureq";
873
-
874
- let inventoryClient = createClient({
875
- baseURL: "https://inventory.internal",
876
- hooks: {
877
- onRequestError: (event) => {
878
- metrics.increment("inventory.request.error", { kind: event.errorKind });
879
- },
880
- },
74
+ // Type-safe GET request
75
+ const result = await api.getJsonResult<User>("/users/:id", {
76
+ params: { id: "42" }
881
77
  });
882
78
 
883
- for (const mw of backendPreset({
884
- retry: { maxRetries: 3, delay: 250 },
885
- circuitBreaker: { failureThreshold: 6, cooldownMs: 30_000 },
886
- })) {
887
- inventoryClient = inventoryClient.use(mw);
79
+ if (result.ok) {
80
+ console.log(result.data.name);
81
+ } else {
82
+ console.error(result.error.kind); // e.g., 'timeout', 'network', 'http'
888
83
  }
889
84
  ```
890
85
 
891
- ---
892
-
893
- ## Adapters & Serializers
894
-
895
- ### Custom adapters
896
-
897
- The default adapter uses the global `fetch`. You can swap it for tests or runtime-specific behavior:
898
-
899
- ```ts
900
- // Test adapter
901
- const testClient = createClient({
902
- adapter: async (url, init) => {
903
- return new Response(JSON.stringify({ ok: true }), {
904
- status: 200,
905
- headers: { "Content-Type": "application/json" },
906
- });
907
- },
908
- });
909
-
910
- // Instrumented adapter
911
- import { createInstrumentedAdapter, fetchAdapter } from "pureq";
912
-
913
- const instrumented = createInstrumentedAdapter(fetchAdapter, {
914
- onStart: (e) => console.log(`→ ${e.url}`),
915
- onSuccess: (e) => console.log(`✓ ${e.durationMs}ms`),
916
- onError: (e) => console.error(`✗ ${e.error}`),
917
- });
918
-
919
- const client = createClient({ adapter: instrumented });
920
- ```
921
-
922
- ### Body serializers
923
-
924
- ```ts
925
- import { createFormUrlEncodedSerializer } from "pureq";
926
-
927
- const client = createClient({
928
- bodySerializer: createFormUrlEncodedSerializer({ arrayMode: "comma" }),
929
- });
930
-
931
- await client.post("/search", { tags: ["ts", "http"], q: "pureq" });
932
- // body: tags=ts%2Chttp&q=pureq
933
- ```
934
-
935
- ### Binary Protocols (MessagePack, Protobuf)
936
-
937
- pureq was designed to be a highly extensible transport layer. To maintain a zero-dependency footprint, we do not bundle heavy binary decoders into the core. However, because pureq fully supports standard `fetch` primitives like `Uint8Array`, integrating binary protocols is natively supported today:
938
-
939
- ```ts
940
- import { encode } from "@msgpack/msgpack";
941
-
942
- // You can swap the body serializer to return binary formats
943
- const msgpackSerializer = (data: unknown) => ({
944
- body: encode(data), // Returns Uint8Array
945
- headers: { "Content-Type": "application/x-msgpack" }
946
- });
947
-
948
- const client = createClient({ bodySerializer: msgpackSerializer });
949
- ```
950
-
951
- **Ecosystem Vision**: Moving forward, rather than bloating the core, we plan to provide official plugin packages like `@pureq/plugin-msgpack` or `@pureq/serialize-protobuf`. This ensures the core model remains pure and lightweight while scaling to meet extreme performance requirements.
952
-
953
- ---
954
-
955
- ## Migration from fetch / axios
956
-
957
- The smallest possible migration:
958
-
959
- ```ts
960
- // Before (fetch)
961
- const response = await fetch("https://api.example.com/users/42");
962
- const user = await response.json();
963
-
964
- // After (pureq)
965
- import { createClient } from "pureq";
966
- const client = createClient({ baseURL: "https://api.example.com" });
967
- const user = await client.getJson<User>("/users/:id", { params: { id: "42" } });
968
- ```
969
-
970
- ### Step-by-step adoption
971
-
972
- 1. **Wrap one API dependency** with `createClient()` — just `baseURL` and `headers`
973
- 2. **Replace repeated `fetch` calls** with `client.get()` / `client.post()`
974
- 3. **Move retry/timeout logic** from ad-hoc `try/catch` into middleware
975
- 4. **Use `*Result` variants** where you want explicit error handling
976
- 5. **Add diagnostics** to gain observability without changing call sites
977
- 6. **Only then** add caching, hedging, circuit breaking, guardrails, or presets
978
-
979
- Each step is independently useful. You don't need everything on day one.
980
-
981
- More details: [Migration guide](./docs/migration_guide.md) · [Codemod recipes](./tools/codemods/README.md)
982
-
983
- ---
984
-
985
- ## Runtime Compatibility
986
-
987
- pureq works anywhere `fetch` is available:
988
-
989
- | Runtime | Supported | Tested |
990
- | ------- | --------- | ------ |
991
- | Node.js 18+ | ✅ | CI matrix (18, 20, 22) |
992
- | Deno | ✅ | — |
993
- | Bun | ✅ | — |
994
- | Modern browsers | ✅ | jsdom smoke test |
995
- | Cloudflare Workers | ✅ | Edge runtime smoke test |
996
- | Vercel Edge | ✅ | Edge runtime smoke test |
997
-
998
- Zero dependencies. ESM-first. Tree-shakeable.
999
-
1000
- More details: [Runtime compatibility matrix](./docs/runtime_compatibility_matrix.md)
1001
-
1002
- ---
1003
-
1004
- ## Production Readiness Checklist
1005
-
1006
- If you are adopting pureq in a production service, start with these defaults:
1007
-
1008
- - Set a request time budget (`defaultTimeout()` or `deadline()`) first.
1009
- - Add `retry()` with explicit status/method policy.
1010
- - Add `validation()` for critical response payloads.
1011
- - Add `circuitBreaker()` for unstable upstreams.
1012
- - Enable diagnostics hooks (`onRequestStart/onRequestSuccess/onRequestError`).
1013
- - Prefer `*Result` APIs on boundary layers to avoid unhandled transport errors.
1014
- - Use `idempotencyKey()` before retrying write operations.
1015
-
1016
- ---
1017
-
1018
- ## API Reference
1019
-
1020
- ### Client
1021
-
1022
- | Method | Returns | Description |
1023
- | ------ | ------- | ----------- |
1024
- | `createClient(options?)` | `PureqClient` | Create a new immutable client |
1025
- | `client.use(middleware)` | `PureqClient` | Add middleware (returns new client) |
1026
- | `client.useRequestInterceptor(fn)` | `PureqClient` | Add request interceptor |
1027
- | `client.useResponseInterceptor(fn)` | `PureqClient` | Add response interceptor |
1028
-
1029
- ### Request Methods
1030
-
1031
- | Method | Throws? | Returns |
1032
- | ------ | ------- | ------- |
1033
- | `client.get(url, opts?)` | Yes | `Promise<HttpResponse>` |
1034
- | `client.getResult(url, opts?)` | Never | `Promise<Result<HttpResponse, PureqError>>` |
1035
- | `client.getJson<T>(url, opts?)` | Yes | `Promise<T>` |
1036
- | `client.getJsonResult<T>(url, opts?)` | Never | `Promise<Result<T, PureqError>>` |
1037
- | `client.post(url, body?, opts?)` | Yes | `Promise<HttpResponse>` |
1038
- | `client.postResult(url, body?, opts?)` | Never | `Promise<Result<HttpResponse, PureqError>>` |
1039
- | `client.postJson<T>(url, body?, opts?)` | Yes | `Promise<T>` |
1040
- | `client.postJsonResult<T>(url, body?, opts?)` | Never | `Promise<Result<T, PureqError>>` |
1041
- | `client.put(...)` / `putResult(...)` | — | Same pattern as post |
1042
- | `client.patch(...)` / `patchResult(...)` | — | Same pattern as post |
1043
- | `client.delete(...)` / `deleteResult(...)` | — | Same pattern as get |
1044
- | `client.fetch(url, init?)` | Yes | Familiar `fetch`-like API |
1045
- | `client.fetchResult(url, init?)` | Never | Result-wrapped fetch-like API |
1046
- | `client.fetchJson<T>(url, init?)` | Yes | fetch + JSON parse |
1047
- | `client.request(config)` | Yes | Low-level full config |
1048
- | `client.requestResult(config)` | Never | Low-level Result variant |
1049
- | `client.requestJson<T>(config)` | Yes | Low-level + JSON |
1050
- | `client.requestJsonResult<T>(config)` | Never | Low-level + JSON + Result |
1051
-
1052
- ### Middleware
1053
-
1054
- | Middleware | Purpose |
1055
- | --------- | ------- |
1056
- | `retry(options)` | Exponential backoff, Retry-After, budget |
1057
- | `authRefresh(options)` | Automatic token refresh (thundering herd prevention) |
1058
- | `deadline(options)` | Total request budget across retries |
1059
- | `defaultTimeout(ms)` | Default per-request timeout |
1060
- | `circuitBreaker(options)` | Fail-fast on repeated failures |
1061
- | `concurrencyLimit(options)` | Cap in-flight requests |
1062
- | `dedupe(options?)` | Collapse duplicate concurrent requests |
1063
- | `hedge(options)` | Duplicate request for tail latency |
1064
- | `validation(options)` | Schema validation bridge (Zod/Valibot ready) |
1065
- | `fallback(options)` | Graceful degradation with fallback values |
1066
- | `httpCache(options)` | In-memory cache with ETag/stale-if-error |
1067
- | `createOfflineQueue(options?)` | Offline mutation queue with replay |
1068
- | `idempotencyKey(options?)` | Auto-inject idempotency headers |
1069
-
1070
- ### Available Presets
1071
-
1072
- | Preset | Best for |
1073
- | ------ | -------- |
1074
- | `resilientPreset()` | General-purpose production stack |
1075
- | `frontendPreset()` | User-facing requests with conservative policy |
1076
- | `bffPreset()` | BFF with auth propagation and upstream stability |
1077
- | `backendPreset()` | Service-to-service under sustained load |
1078
-
1079
- ### Observability Exports
1080
-
1081
- | Export | Purpose |
1082
- | ------ | ------- |
1083
- | `createMiddlewareDiagnostics(options)` | Collect metrics and traces |
1084
- | `createConsoleDiagnosticsExporter()` | Console logging exporter |
1085
- | `createOpenTelemetryDiagnosticsExporter(meter)` | OTel metrics exporter |
1086
- | `mapToStandardHttpAttributes(event)` | OTel semantic conventions |
1087
- | `mapToAwsSemanticConventions(event)` | AWS X-Ray attributes |
1088
- | `mapToGcpSemanticConventions(event)` | GCP Cloud Trace attributes |
1089
- | `redactHeaders(headers, options?)` | Redact sensitive headers |
1090
- | `redactObjectFields(obj, options?)` | Redact fields by pattern |
1091
- | `redactUrlQueryParams(url, options?)` | Redact URL query params |
1092
- | `piiRedactionOptions` | Pre-built PII redaction profile |
1093
- | `authRedactionOptions` | Pre-built auth redaction profile |
1094
-
1095
- ### Adapters and Serializers
1096
-
1097
- | Export | Purpose |
1098
- | ------ | ------- |
1099
- | `fetchAdapter` | Default global fetch adapter |
1100
- | `createInstrumentedAdapter(base, hooks)` | Adapter with lifecycle hooks |
1101
- | `jsonBodySerializer` | Default JSON body serializer |
1102
- | `createFormUrlEncodedSerializer(options?)` | Form URL-encoded serializer |
1103
-
1104
- ---
1105
-
1106
- ## Security
1107
-
1108
- pureq takes a defense-in-depth approach to transport layer security:
1109
-
1110
- - **Type-safe Path Templates**: `/users/:id` inherently protects against accidental payload leakage or malformed URL construction compared to manual string interpolation.
1111
- - **Resource Exhaustion Defense**: Middleware like `deadline()`, `defaultTimeout()`, and `concurrencyLimit()` help mitigate backend overloading and "Slow Loris" style denial-of-service on the client.
1112
- - **Telemetry Safe-by-Default**: Use built-in diagnostics exports with `redactIndicators`, `redactHeaders`, and `redactObjectFields` to prevent PII and authentication tokens from inadvertently entering server logs or APM dashboards.
1113
- - **Explicit Serialization**: Defining body serializers restricts accidental serialization of unintended properties compared to ad-hoc `JSON.stringify`.
1114
-
1115
- *Note: Standard browser-based security constructs like CSRF tokens and CORS remain the responsibility of the underlying `fetch` implementation. pureq stays out of your way and lets standard headers handle web platform security.*
1116
-
1117
- ---
1118
-
1119
- ## Development
1120
-
1121
- ```bash
1122
- npm run typecheck # type checking
1123
- npm test # all tests
1124
- npm run test:unit # lightweight unit tests (heavy load tests excluded)
1125
- npm run test:stress # baseline stress test
1126
- npm run test:heavy # heavy load + shock + chaos + invariant-fuzz tests (nightly-recommended)
1127
- npm run test:nightly # alias of test:heavy
1128
- npm run test:ci # lightweight CI: unit + integration + contract + stress + typecheck
1129
- npm run test:ci:full # CI + heavy tests
1130
- npm run test:browser # browser runtime smoke test
1131
- npm run test:edge # edge runtime smoke test
1132
- npm run build # production build
1133
- npm run benchmark # performance benchmark
1134
- ```
1135
-
1136
- ## Limitations
1137
-
1138
- pureq is intentionally focused. A few limits are worth knowing:
1139
-
1140
- - `httpCache()` is in-memory and process-local — not a distributed cache
1141
- - `hedge()` duplicates requests and should only be used for idempotent reads
1142
- - `circuitBreaker()` is per-process — for distributed circuit breaking use an external store
1143
- - JSON helpers are convenience methods, not a replacement for your domain model
1144
- - Diagnostics exporters are lightweight adapters, not a full telemetry SDK
1145
-
1146
- These limits are by design. The library is meant to stay small, explicit, and composable.
1147
-
1148
- ## Documentation
1149
-
1150
- | Document | Content |
1151
- | -------- | ------- |
1152
- | [Reliability Primitives](./docs/reliability_primitives.md) | retry, deadline, hedge, circuit breaker |
1153
- | [Cache & Offline](./docs/cache_and_offline.md) | httpCache, offlineQueue, stale-if-error |
1154
- | [Observability & Governance](./docs/observability_and_governance.md) | diagnostics, OTel, redaction, guardrails |
1155
- | [React Query Integration](./docs/integration_react_query.md) | detailed React Query patterns |
1156
- | [SWR Integration](./docs/integration_swr.md) | detailed SWR patterns |
1157
- | [BFF & Backend Templates](./docs/templates_bff_backend.md) | per-dependency client patterns |
1158
- | [Positioning](./docs/positioning_react_query_swr_bff_backend.md) | when to use what |
1159
- | [Migration Guide](./docs/migration_guide.md) | fetch/axios → pureq step by step |
1160
- | [Benchmarks](./docs/benchmarks.md) | methodology and baseline numbers |
1161
- | [Runtime Compatibility](./docs/runtime_compatibility_matrix.md) | supported runtimes |
1162
- | [Adoption Strategy](./docs/standard_adoption_strategy.md) | org-wide rollout playbook |
1163
-
1164
86
  ## License
1165
87
 
1166
- [MIT](./LICENSE.md)
88
+ MIT © [Shihiro](https://github.com/shiro-shihi)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pureq/pureq",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Functional, immutable, and type-safe HTTP client layer with middleware composition.",
5
5
  "main": "./dist/src/index.js",
6
6
  "types": "./dist/src/index.d.ts",