Skip to content

Commit 3356fba

Browse files
Update README with new features and details
Enhanced the README with additional features and details about the openFetch client, including structured debug logging and JSON validation.
1 parent 8a75416 commit 3356fba

1 file changed

Lines changed: 162 additions & 6 deletions

File tree

README.md

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,164 @@
77
</p>
88

99
# @hamdymohamedak/openfetch
10+
<p align="center">
11+
<img
12+
src="https://cdn.jsdelivr.net/npm/@hamdymohamedak/openfetch@latest/docs/openfetch-logo.jpg"
13+
alt="openFetch official logo"
14+
width="400"
15+
/>
16+
</p>
17+
18+
# @hamdymohamedak/openfetch
19+
20+
A small, dependency-free HTTP client for JavaScript runtimes that expose the standard [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. It supports instances with defaults, request and response interceptors, HTTP verb helpers, optional request/response transforms, composable middleware, retries, structured debug logging, optional JSON validation ([Standard Schema](https://github.com/standard-schema/standard-schema)), and in-memory caching—without legacy browser-only globals.
21+
22+
**What you get**
23+
24+
- **ESM-only** — use `import` / `import type`; there is no CommonJS build. **1.x** follows [semantic versioning](https://semver.org/).
25+
- One transport: `fetch` only (Node 18+, Bun, Deno, Cloudflare Workers, browsers).
26+
- No polyfills required for supported environments.
27+
- Safe for server rendering and React Server Components: no `window`, `document`, `localStorage`, or framework coupling.
28+
29+
## Installation
30+
31+
```bash
32+
npm install @hamdymohamedak/openfetch
33+
# or pin the stable major line:
34+
npm install @hamdymohamedak/openfetch@^1
35+
```
36+
37+
## Quick start
38+
39+
```ts
40+
import openFetch, { createClient } from "@hamdymohamedak/openfetch";
41+
42+
const { data, status, headers } = await openFetch.get(
43+
"https://api.example.com/v1/users"
44+
);
45+
46+
const api = createClient({
47+
baseURL: "https://api.example.com",
48+
headers: { Authorization: "Bearer <token>" },
49+
timeout: 10_000,
50+
unwrapResponse: true,
51+
});
52+
53+
const users = await api.get("/v1/users");
54+
```
55+
56+
## Features
57+
58+
| Area | Details |
59+
|------|---------|
60+
| Instances | `createClient()` / `create()` with mutable `defaults` |
61+
| HTTP verbs | `request`, `get`, `post`, `put`, `patch`, `delete`, `head`, `options` |
62+
| Config | `baseURL`, `params`, `headers`, `timeout`, `signal`, `data` / `body`, `auth`, `responseType`, `rawResponse`, `validateStatus`, `throwHttpErrors`, `jsonSchema` (Standard Schema), `init` hooks, `debug` / `logger` (pipeline events), `assertSafeUrl`, `unwrapResponse`, plus `RequestInit` fields (`credentials`, `redirect`, …) |
63+
| Interceptors | Request and response stacks (documented call order) |
64+
| Middleware | Async `use()` hooks wrapping the fetch adapter |
65+
| Errors | `OpenFetchError` with `toShape()` / `toJSON()`; `SchemaValidationError` when `jsonSchema` fails |
66+
| Retry | `createRetryMiddleware()` — backoff, `timeoutTotalMs` / `timeoutPerAttemptMs`, idempotent POST key; `OpenFetchForceRetry` from `hooks({ onAfterResponse })` to force another attempt |
67+
| Cache | `MemoryCacheStore` + `createCacheMiddleware()` (TTL, optional stale-while-revalidate) |
68+
| Plugins | `retry({ attempts, … })`, `timeout(ms)`, `hooks({ onBeforeRetry, onAfterResponse, … })`, `debug({ maskStrategy: 'partial' \| 'hash', … })`, `strictFetch()` |
69+
| Fluent API | `createFluentClient()` — lazy chain; **each** `.json()` / `.json(schema)` / `.raw()` / … runs **one** request unless you use `.memo()`; `.raw()``Response` |
70+
71+
Subpath imports (tree-shaking): `@hamdymohamedak/openfetch/plugins`, `@hamdymohamedak/openfetch/sugar`.
72+
73+
```ts
74+
import { createFluentClient, retry, timeout } from "@hamdymohamedak/openfetch";
75+
76+
const client = createFluentClient({ baseURL: "https://api.example.com" })
77+
.use(retry({ attempts: 3 }))
78+
.use(timeout(5000));
79+
80+
const data = await client("/v1/user").json<{ id: string }>();
81+
82+
// Native Response (unread body); second terminal = second HTTP call
83+
const res = await client("/v1/export").get().raw();
84+
const blob = await res.blob();
85+
86+
// One HTTP round-trip, then parse as JSON and as text (body buffered once — not HTTP caching)
87+
const memoed = client("/v1/profile").get().memo();
88+
const profile = await memoed.json();
89+
const rawText = await memoed.text();
90+
```
91+
92+
Register **`retry` before `timeout`** so retries wrap the full inner stack. Use **interceptors** to mutate config/response; use **`hooks`** for side-effect logging around the middleware pipeline.
93+
94+
**Fluent:** `.get()` / `.post()` only build config. **Each terminal** (`.json()`, `.text()`, `.send()`, `.raw()`, …) triggers a **new** `fetch` unless the chain used **`.memo()`** (request-level memoization: one `fetch`, body read once into memory). For two reads of the same native `Response`, use **`cloneResponse(res)`** from the package exports (or `.clone()` on the `Response`).
95+
96+
**`rawResponse` / `.raw()`:** the adapter does **not** read the body and skips **`transformResponse`**. Client **response interceptors** still run (`data` is the native `Response`). Middleware that expects parsed `ctx.response.data` will not see transforms until you parse yourself.
97+
98+
**Retry timing:** `retry.timeoutTotalMs` measures elapsed time with a monotonic clock (`performance.now()` when available), so the budget is not skewed by system clock changes. By default (`retry.enforceTotalTimeout !== false`), each attempt merges a deadline into the request `signal` so an in-flight `fetch` aborts when the budget runs out (`ERR_RETRY_TIMEOUT`). Set `retry.enforceTotalTimeout: false` to enforce the budget only between attempts. `retry.timeoutPerAttemptMs` sets `timeout` for every attempt inside the retry middleware. Each `dispatch` uses `clearTimeout` in a `finally` block so per-attempt timers are not left dangling.
99+
100+
**Debug:** Default logs omit request headers. Logged URLs **redact common sensitive query parameters** (`token`, `code`, `password`, …); set `maskUrlQuery: false` to log raw URLs (avoid in production). Use `debug({ includeRequestHeaders: true, maskHeaders: ["authorization"], maskStrategy: "partial" })` for values like `Bearer ****abcd`, or `maskStrategy: "hash"` for a short fingerprint. **`maskHeaderValues`** supports the same strategies when building your own logs.
101+
102+
### Execution model
103+
104+
Understanding order helps avoid surprises with retries, timeouts, and escape hatches.
105+
106+
1. **Request interceptors** run on the merged config (mutations apply to the in-flight request).
107+
2. **Middleware stack** runs in registration order: the **first** `use()` is the **outer** shell; its `next()` enters the next middleware, and the **last** middleware’s `next()` runs the built-in handler that calls **`dispatch`** (`fetch` + parse, unless `rawResponse`).
108+
3. **Inside `dispatch`:** `transformRequest``fetch` → (optional body parse) → **`transformResponse`** (skipped when `rawResponse`).
109+
4. **Response interceptors** run on the `OpenFetchResponse` (for `rawResponse`, `data` is still a native `Response`).
110+
5. **Retry** (`createRetryMiddleware` / `retry()`): each retry calls `next()` again, so middleware **below** retry in the stack runs **once per attempt**; middleware **above** retry wraps the whole loop (one outer enter/exit per logical request).
111+
6. **Terminal methods** (fluent `.json()`, `.text()`, client `.get()`, …) each start a **new** pipeline invocation unless you used **`.memo()`** on that chain.
112+
113+
**Backoff:** between retries, the retry middleware sleeps with jitter; if the request **`signal`** aborts during that wait, the loop stops (`ERR_CANCELED`).
114+
115+
### Memory cache and authentication
116+
117+
The default cache key is ``METHOD fullUrl``. The first request with **`Authorization` or `Cookie`** and no `varyHeaderNames` / custom `key` triggers a **one-time `console.warn`** (suppress with `suppressAuthCacheKeyWarning: true` if you only cache public data). For **authenticated or per-user** GETs, also pass header names that affect the response so entries do not leak across users:
118+
119+
```ts
120+
createCacheMiddleware(store, {
121+
ttlMs: 60_000,
122+
varyHeaderNames: ["authorization", "cookie"],
123+
});
124+
```
125+
126+
Or build a custom `key` and use `appendCacheKeyVaryHeaders` from the package exports. See [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md).
127+
128+
### Retries and POST/PUT
129+
130+
By default, retries after network failures or retryable HTTP statuses run only for **GET**, **HEAD**, **OPTIONS**, and **TRACE**. To retry mutating methods, set `retry: { retryNonIdempotentMethods: true }` (per client or per request).
131+
132+
When `retryNonIdempotentMethods` is true and `maxAttempts > 1`, **POST** requests automatically receive a stable **`Idempotency-Key`** header (if you did not set one) so retries share the same key (Stripe-style deduplication). Opt out with `retry: { autoIdempotencyKey: false }`. You can still set `Idempotency-Key` / `idempotency-key` yourself; it will be respected.
133+
134+
If the request `signal` is aborted (`AbortController.abort()`), the retry middleware stops: no more `fetch` attempts, and backoff ends early when a signal is linked.
135+
136+
For low-level access without consuming the body in openFetch, set `rawResponse: true` on a request or use fluent `.raw()`.
137+
138+
### Optional URL guard (server-side)
139+
140+
For URLs influenced by untrusted input, either call `assertSafeHttpUrl(url)` before requesting or enable **`assertSafeUrl: true`** on the client (defaults or per request). That blocks literal private/loopback IPs for `http:`/`https:` on the fully resolved URL; it does not fix DNS rebinding — see [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md).
141+
142+
### Errors and logging
143+
144+
`OpenFetchError.toShape()` / `toJSON()` omit `config.auth` and, **by default**, omit response **`data`** and **`headers`**; pass `includeResponseData: true` / `includeResponseHeaders: true` when you need them for trusted diagnostics. By default the serialized `url` **redacts common sensitive query parameters**; pass `redactSensitiveUrlQuery: false` only in trusted environments. The error instance itself can still hold full `config`; do not expose it raw.
145+
146+
## Documentation
147+
148+
- **Guide (VitePress):** [openfetch-js.github.io/openfetch-docs/](https://openfetch-js.github.io/openfetch-docs/)
149+
- **Changelog:** [CHANGELOG.md](https://github.com/openfetch-js/OpenFetch/blob/main/CHANGELOG.md)
150+
- **Security:** [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md)
151+
- **Claude Code:** `claude plugin marketplace add openfetch-js/OpenFetch`, then `claude plugin install openfetch@openfetch-js`. Published skill plugin: [openFetchSkill — README](https://github.com/openfetch-js/openFetchSkill/blob/main/README.md).
152+
- **Skill folder template (this monorepo):** [examples/claude-skill](https://github.com/openfetch-js/OpenFetch/tree/main/examples/claude-skill) — layout reference; see [examples/README.md](https://github.com/openfetch-js/OpenFetch/blob/main/examples/README.md).
153+
- **Contributing:** [CONTRIBUTING.md](https://github.com/openfetch-js/OpenFetch/blob/main/CONTRIBUTING.md)
154+
155+
## Requirements
156+
157+
- Node.js **18** or newer (or any runtime with `fetch` and `AbortController`).
158+
159+
## License
160+
161+
MIT
10162

11-
A small, dependency-free HTTP client for JavaScript runtimes that expose the standard [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. It supports instances with defaults, request and response interceptors, HTTP verb helpers, optional request/response transforms, composable middleware, retries, and in-memory caching—without legacy browser-only globals.
163+
A small, dependency-free HTTP client for JavaScript runtimes that expose the standard [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. It supports instances with defaults, request and response interceptors, HTTP verb helpers, optional request/response transforms, composable middleware, retries, structured debug logging, optional JSON validation ([Standard Schema](https://github.com/standard-schema/standard-schema)), and in-memory caching—without legacy browser-only globals.
12164

13165
**What you get**
14166

167+
- **ESM-only** — use `import` / `import type`; there is no CommonJS build. **1.x** follows [semantic versioning](https://semver.org/).
15168
- One transport: `fetch` only (Node 18+, Bun, Deno, Cloudflare Workers, browsers).
16169
- No polyfills required for supported environments.
17170
- Safe for server rendering and React Server Components: no `window`, `document`, `localStorage`, or framework coupling.
@@ -20,6 +173,8 @@ A small, dependency-free HTTP client for JavaScript runtimes that expose the sta
20173

21174
```bash
22175
npm install @hamdymohamedak/openfetch
176+
# or pin the stable major line:
177+
npm install @hamdymohamedak/openfetch@^1
23178
```
24179

25180
## Quick start
@@ -47,14 +202,14 @@ const users = await api.get("/v1/users");
47202
|------|---------|
48203
| Instances | `createClient()` / `create()` with mutable `defaults` |
49204
| HTTP verbs | `request`, `get`, `post`, `put`, `patch`, `delete`, `head`, `options` |
50-
| Config | `baseURL`, `params`, `headers`, `timeout`, `signal`, `data` / `body`, `auth`, `responseType`, `rawResponse`, `validateStatus` |
205+
| Config | `baseURL`, `params`, `headers`, `timeout`, `signal`, `data` / `body`, `auth`, `responseType`, `rawResponse`, `validateStatus`, `throwHttpErrors`, `jsonSchema` (Standard Schema), `init` hooks, `debug` / `logger` (pipeline events), `assertSafeUrl`, `unwrapResponse`, plus `RequestInit` fields (`credentials`, `redirect`, …) |
51206
| Interceptors | Request and response stacks (documented call order) |
52207
| Middleware | Async `use()` hooks wrapping the fetch adapter |
53-
| Errors | `OpenFetchError` with `toShape()` / `toJSON()` for structured logging |
54-
| Retry | `createRetryMiddleware()` — backoff, `timeoutTotalMs` / `timeoutPerAttemptMs`, idempotent POST key |
208+
| Errors | `OpenFetchError` with `toShape()` / `toJSON()`; `SchemaValidationError` when `jsonSchema` fails |
209+
| Retry | `createRetryMiddleware()` — backoff, `timeoutTotalMs` / `timeoutPerAttemptMs`, idempotent POST key; `OpenFetchForceRetry` from `hooks({ onAfterResponse })` to force another attempt |
55210
| Cache | `MemoryCacheStore` + `createCacheMiddleware()` (TTL, optional stale-while-revalidate) |
56-
| Plugins | `retry({ attempts })`, `timeout(ms)`, `hooks(...)`, `debug({ maskStrategy: 'partial' \| 'hash', … })`, `strictFetch()` |
57-
| Fluent API | `createFluentClient()` — lazy chain; **each** `.json()` / `.raw()` / … runs **one** request unless you use `.memo()`; `.raw()``Response` |
211+
| Plugins | `retry({ attempts, … })`, `timeout(ms)`, `hooks({ onBeforeRetry, onAfterResponse, … })`, `debug({ maskStrategy: 'partial' \| 'hash', … })`, `strictFetch()` |
212+
| Fluent API | `createFluentClient()` — lazy chain; **each** `.json()` / `.json(schema)` / `.raw()` / … runs **one** request unless you use `.memo()`; `.raw()``Response` |
58213

59214
Subpath imports (tree-shaking): `@hamdymohamedak/openfetch/plugins`, `@hamdymohamedak/openfetch/sugar`.
60215

@@ -134,6 +289,7 @@ For URLs influenced by untrusted input, either call `assertSafeHttpUrl(url)` bef
134289
## Documentation
135290

136291
- **Guide (VitePress):** [openfetch-js.github.io/openfetch-docs/](https://openfetch-js.github.io/openfetch-docs/)
292+
- **Changelog:** [CHANGELOG.md](https://github.com/openfetch-js/OpenFetch/blob/main/CHANGELOG.md)
137293
- **Security:** [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md)
138294
- **Claude Code:** `claude plugin marketplace add openfetch-js/OpenFetch`, then `claude plugin install openfetch@openfetch-js`. Published skill plugin: [openFetchSkill — README](https://github.com/openfetch-js/openFetchSkill/blob/main/README.md).
139295
- **Skill folder template (this monorepo):** [examples/claude-skill](https://github.com/openfetch-js/OpenFetch/tree/main/examples/claude-skill) — layout reference; see [examples/README.md](https://github.com/openfetch-js/OpenFetch/blob/main/examples/README.md).

0 commit comments

Comments
 (0)