diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 413d263..bf80a4c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -45,3 +45,12 @@ jobs: - name: Run tests run: npm test + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ./packages/httio/coverage/lcov.info,./packages/rest/coverage/lcov.info + flags: httio,rest + slug: vladstsk/httio diff --git a/README.md b/README.md index aee052d..77241c7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # **Httio** +[![Bundle coverage](https://codecov.io/gh/vladstsk/httio/branch/main/graph/badge.svg?flag=httio)](https://codecov.io/gh/vladstsk/httio?flag=httio) [![Bundle size](https://img.shields.io/bundlephobia/minzip/httio)](https://bundlephobia.com/package/httio) -[![License](https://img.shields.io/npm/l/httio)](https://github.com/vladstsk/httio/blob/main/LICENSE) +[![Bundle tree-shaking](https://badgen.net/bundlephobia/tree-shaking/httio)](https://bundlephobia.com/package/httio) [![Typed with TypeScript](https://badgen.net/npm/types/httio)](https://github.com/vladstsk/httio) +[![License](https://img.shields.io/npm/l/httio)](https://github.com/vladstsk/httio/blob/main/LICENSE) > Lightweight, type-safe HTTP client for browsers and Node.js. > Built on top of the native `fetch` but provides a better DX with strong typing, middleware and a minimalistic API. @@ -12,31 +14,68 @@ ## Table of Contents 1. [Why Httio?](#why-httio) -2. [Installation](#installation) -3. [Quick Start](#quick-start) -4. [Type-Safe Requests](#type-safe-requests) -5. [Client Configuration](#client-configuration) -6. [Handling Responses](#handling-responses) -7. [Middleware](#middleware) -8. [Error Handling](#error-handling) -9. [Recipes](#recipes) -10. [API Reference](#api-reference) -11. [FAQ](#faq) -12. [License](#license) +2. [Httio vs. The Rest](#httio-vs-the-rest) +3. [Installation](#installation) +4. [Quick Start](#quick-start) + - [ES Modules (`import`)](#es-modules-import) + - [CommonJS (`require`)](#commonjs-require) +5. [Type-Safe Requests](#type-safe-requests) +6. [Client Configuration](#client-configuration) + - [Base client](#base-client) + - [Extending an existing client](#extending-an-existing-client) + - [Supported options](#supported-options) +7. [Handling Responses](#handling-responses) +8. [Middleware](#middleware) +9. [Error Handling](#error-handling) +10. [Recipes](#recipes) + - [Passing query params](#passing-query-params) + - [Uploading files](#uploading-files) + - [Working with streams](#working-with-streams) + - [Reusing an `AbortController`](#reusing-an-abortcontroller) +11. [API Reference](#api-reference) + - [`client(options?)`](#clientoptions) + - [HttioClient` instance](#httioclient-instance) + - [`RequestOptions`](#requestoptions) +12. [FAQ](#faq) +13. [License](#license) --- ## Why Httio? -| Feature | Description | -|---------------------------|-------------------------------------------------------------------------------------------------------| -| **TypeScript-first** | All public types are exported; strict compile-time checks. | -| **Tiny footprint** | Ships as ESM + CJS, zero runtime dependencies. | -| **Lazy parsing** | Body is not parsed automatically—_you_ decide when and how. | +| Feature | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------| +| **TypeScript-first** | All public types are exported; strict compile-time checks. | +| **Tiny footprint** | Ships as ESM + CJS, zero runtime dependencies. | +| **Lazy parsing** | Body is not parsed automatically—_you_ decide when and how. | | **One interface everywhere** | Works in browsers, Node 18+, edge functions—no polyfills required. | -| **Extensible** | Middleware chain for logging, auth, caching, etc. | -| **Convenient cloning** | `extends()` lets you reuse and override base options elegantly. | -| **Full control** | Everything from `fetch` is exposed plus syntactic sugar (`params`, `json`, `timeout`). | +| **Extensible** | Middleware chain for logging, auth, caching, etc. | +| **Convenient cloning** | `extends()` lets you reuse and override base options elegantly. | +| **Full control** | Everything from `fetch` is exposed plus syntactic sugar (`params`, `json`, `timeout`). | + +--- + +## Httio vs. The Rest + +| Library | Size
(min + gzip) | TS‑first | Browser | Node | Native `fetch` | Middleware | Retries | Deps | Notes | +|------------------|:--------------------------------------------------------------------------:|:--------------:|:-------:|:----:|:-------------------:|:----------:|:-------:|:--------------------------------------------------------------------------------------------------------------------------------------:|-------------------------------| +| **Httio** | ![size](https://img.shields.io/bundlephobia/minzip/httio?label) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | [![deps](https://badgen.net/bundlephobia/dependency-count/httio?label)](https://bundlephobia.com/package/httio) | Modern, tiny | +| Axios | ![size](https://img.shields.io/bundlephobia/minzip/axios?label) | ⚠️ | ✅ | ✅ | ❌ | ⚠️ | ⚠️ | [![deps](https://badgen.net/bundlephobia/dependency-count/axios?label)](https://bundlephobia.com/package/axios) | Heavy | +| isomorphic-fetch | ![size](https://img.shields.io/bundlephobia/minzip/isomorphic-fetch?label) | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | [![deps](https://badgen.net/bundlephobia/dependency-count/isomorphic-fetch?label)](https://bundlephobia.com/package/isomorphic-fetch) | Simple shim | +| ky | ![size](https://img.shields.io/bundlephobia/minzip/ky?label) | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | [![deps](https://badgen.net/bundlephobia/dependency-count/ky?label)](https://bundlephobia.com/package/ky) | Small, fetch‑first | +| superagent | ![size](https://img.shields.io/bundlephobia/minzip/superagent?label) | ⚠️ | ✅ | ✅ | ❌ | ✅ | ⚠️ | [![deps](https://badgen.net/bundlephobia/dependency-count/superagent?label)](https://bundlephobia.com/package/superagent) | Classic | +| request | ![size](https://img.shields.io/bundlephobia/minzip/request?label) | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | [![deps](https://badgen.net/bundlephobia/dependency-count/request?label)](https://bundlephobia.com/package/request) | Deprecated | +| r2 | ![size](https://img.shields.io/bundlephobia/minzip/r2?label) | ⚠️ | ❌ | ✅ | ✅ | ❌ | ❌ | [![deps](https://badgen.net/bundlephobia/dependency-count/r2?label)](https://bundlephobia.com/package/r2) | Minimal | +| phin | ![size](https://img.shields.io/bundlephobia/minzip/phin?label) | ⚠️ | ❌ | ✅ | ❌ | ❌ | ✅ | [![deps](https://badgen.net/bundlephobia/dependency-count/phin?label)](https://bundlephobia.com/package/phin) | Promise client | + + +### Key takeaways + +1. **Native by design** – Httio is a thin layer above `fetch`, so knowledge transfers instantly and no shims are required in modern runtimes. +2. **Strict types everywhere** – every public method is generically typed, turning many runtime bugs into compile-time errors. +3. **Zero runtime deps** – smaller bundles, faster cold starts, less supply-chain risk. +4. **Middleware without bloat** – add logging, auth or caching in a few lines, no heavyweight “interceptors” machinery. +5. **One client → any environment** – the same code runs in browsers, Node 18+, edge workers and service workers. --- @@ -60,26 +99,51 @@ For earlier versions add any fetch polyfill. ## Quick Start +### ES Modules (`import`) + ```ts import httio from 'httio'; // GET const users = await httio.get('https://api.example.com/users').json(); +const payload = { + name: 'Alice', + email: 'alice@example.com', +}; + // POST -await httio.post( - 'https://api.example.com/users', - { name: 'Alice', email: 'alice@example.com' } -).json(); +await httio.post('https://api.example.com/users', payload); // PUT with query params -await httio.put( - 'https://api.example.com/users/42', - { name: 'Bob' }, - { params: { notify: true } } -); +await httio.put('https://api.example.com/users/42', payload, { + params: { + notify: true, + }, +}); ``` +### CommonJS (`require`) + +If your project is still on CommonJS (for example when you run Node ≤ 12 or simply prefer `require`), import the library like this: + +```javascript +// Option 1 — named export (recommended) +const { client } = require('httio'); + +// Option 2 — default export (identical to ESM default) +const httio = require('httio'); + +// Example +const api = client({ + url: 'https://api.example.com', +}); + +api.get('/users/me').json().then(console.log); +``` + +`httio` ships with dual ESM/CJS bundles (`index.mjs` & `index.cjs`), so no extra Babel or `type: "module"` setup is required. + --- ## Type-Safe Requests @@ -131,35 +195,46 @@ const api = client({ `extends()` returns a _new_ instance with inherited and/or overridden options: ```ts -const v2 = api.extends({ url: '/v2' }); -await v2.get('/status'); // → https://api.example.com/v2/status +const v2 = api.extends({ + url: '/v2', +}); + +await v2.get('/status'); // → https://api.example.com/v2/status ``` ### Supported options -| Option | Type | Default | Description | -|-----------|----------------------------------------|------------------------------------|-------------------------------------------------| -| `url` | `string \| URL` | – | Base URL for relative paths. | -| `headers` | `HeadersInit` | – | Global headers. | -| `params` | `Record` | – | Query params (auto-encoded). | -| `timeout` | `number` | – | Abort request after N ms via `AbortController`. | -| `url` | `string \| URL` (only in `extends`) | – | Alias for `base` when only host changes. | -| `fetch` | `(input, init) => Promise` | `globalThis.fetch \| window.fetch` | Custom fetch implementation (handy in tests). | +| Option | Type | Default | Description                                    | +|-----------|-----------------------------------------------------------------|-----------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `url` | `string \| URL` | – | Base URL for relative paths. | +| `url` | `string \| URL` (only in `extends`) | – | Extends the base URL or replaces it if the link contains a host. | +| `params` | Record<string, string \| number> | – | Query params (auto-encoded). | +| `timeout` | `number` | `1000` | Abort request after N ms. | +| `retry` | `number \| RetryOptions` | { limit: 3, delay: 1000 } | How many times (and with what delay) to retry failed requests. | +| `fetch` | `Fetcher` | globalThis.fetch \| window.fetch | Custom fetch implementation (handy in tests). | + +> You can also pass any field accepted by the standard Fetch `Request`/`Response` objects; those options are forwarded unchanged. --- ## Handling Responses -Httio returns a **wrapper** around `Response` with extra parsing helpers. -They are _lazy_—HTTP call is sent immediately, but body reading starts only when a parser is invoked. +Httio returns a **wrapper** of type `HttioBody & Promise` around the native `Response`. + +It behaves as both: + +1. `Promise` – you can `await` it or call `.then()`. +2. `HttioBody` – exposes lazy body-parsers: `json()`, `text()`, `blob()`, `buffer()`, `bytes()`, `stream()`. -| Method | Return type | -|------------------|------------------------| -| `json()` | `Promise` | -| `text()` | `Promise` | -| `blob()` | `Promise` | -| `bytes()` | `Promise` | -| `buffer()` | `Promise` | +The HTTP request is dispatched immediately, while the response body is read only when one of the parsers is invoked. + +| Method | Return type | +|------------------|---------------------------| +| `json()` | `Promise` | +| `text()` | `Promise` | +| `blob()` | `Promise` | +| `bytes()` | `Promise` | +| `buffer()` | `Promise` | | `stream()` | `Promise` | Example of deferred parsing: @@ -179,23 +254,23 @@ const data = await response.json(); ## Middleware -Middleware are async functions `(req, next) => Response` executed in a chain: +Middleware are async functions `(request: HttioRequest, next: NextMiddleware): MaybePromise` executed in a chain: ```ts import httio, { type Middleware } from 'httio'; const auth: Middleware = async (req, next) => { req.headers.set('Authorization', `Bearer ${getToken()}`); - + return next(req); }; const logger: Middleware = async (req, next) => { const started = performance.now(); - const res = await next(req); + console.log(`${req.method} ${req.url} → ${res.status} (${Date.now() - started} ms)`); - + return res; }; @@ -213,34 +288,6 @@ HTTP status `4xx/5xx` triggers a `HttpError`: ```ts import httio, { HttpError } from 'httio'; -try { - await httio.get('/admin').json(); -} catch (err) { - if (err instanceof HttpError) { - console.error(`Error ${err.status}: ${err.message}`); - } else { - throw err; // non-HTTP error - } -} -``` - -`HttpError` exposes: - -| Property | Description | -|-------------|----------------------------------| -| `status` | HTTP status code | -| `statusText`| Human-readable text (if any) | -| `response` | Original `Response` object | -| `url` | Final URL (after redirects) | - ---- - -## Error Handling - -HTTP status codes in the `4xx/5xx` range throw an instance of `HttpError`: -```ts -import httio, { HttpError } from 'httio'; - try { await httio.get('/admin').json(); } catch (err) { @@ -257,14 +304,14 @@ try { } ``` -`HttpError` exposes only two properties: +`HttpError` extends the native `Error` class and additionally exposes: -| Property | Type | Description | -|-----------|-----------------|------------------------------------------| -| `request` | `HttioRequest` | Object representing the original request | -| `response`| `HttioResponse` | Object representing the server response | +| Property | Type | Description | +|------------|-----------------|---------------------------------------------------------------| +| `request` | `HttioRequest` | The request object that triggered the error (post-middleware) | +| `response` | `HttioResponse` | The received response (contains status, headers, etc.) | -You can read any additional details (status code, headers, body, etc.) from the `response` object itself. +Standard `Error` fields (`name`, `message`, `stack`) remain available. --- @@ -297,11 +344,11 @@ const reader = stream.getReader(); while (true) { const { value, done } = await reader.read(); - + if (done) { break; } - + console.log(new TextDecoder().decode(value)); } ``` @@ -327,20 +374,21 @@ See [configuration](#supported-options) for the list of parameters. ### `HttioClient` instance -| Method | Description | -|-------------------------------------|------------------------------------------------| +| Method | Description | +|-------------------------------------|----------------------------------------------------------------------| | `get(url, opts?)` | `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` — same signature | -| `extends(opts)` | Returns a new client inheriting options | -| `use(...middleware)` | Adds middleware | +| `extends(opts)` | Returns a new client inheriting options | +| `use(...middleware)` | Adds middleware | ### `RequestOptions` Extends the native `RequestInit`: -| Extra field | Type | Description | -|-------------|---------------------------------|--------------------------------| -| `params` | `Record` | Adds query parameters | -| `timeout` | `number` | Aborts the request after N ms | +| Extra field | Type | Description | +|-------------|-----------------------------------------------------------------|----------------------------------------------------------------| +| `params` | Record<string, string \| number> | Adds query parameters | +| `timeout` | `number` | Aborts the request after N ms | +| `retry` | `number \| RetryOptions` | How many times (and with what delay) to retry failed requests. | ### Types @@ -373,7 +421,10 @@ import { client } from 'httio'; import { createFetchMock } from '@mswjs/interceptors'; const fetchMock = createFetchMock(); -const api = client({ fetch: fetchMock }); + +const api = client({ + fetch: fetchMock, +}); ``` --- diff --git a/packages/httio/README.md b/packages/httio/README.md index fc21c87..f2e3850 100644 --- a/packages/httio/README.md +++ b/packages/httio/README.md @@ -1,8 +1,10 @@ # **Httio** +[![Bundle coverage](https://codecov.io/gh/vladstsk/httio/branch/main/graph/badge.svg?flag=httio)](https://codecov.io/gh/vladstsk/httio?flag=httio) [![Bundle size](https://img.shields.io/bundlephobia/minzip/httio)](https://bundlephobia.com/package/httio) -[![License](https://img.shields.io/npm/l/httio)](https://github.com/vladstsk/httio/blob/main/LICENSE) +[![Bundle tree-shaking](https://badgen.net/bundlephobia/tree-shaking/httio)](https://bundlephobia.com/package/httio) [![Typed with TypeScript](https://badgen.net/npm/types/httio)](https://github.com/vladstsk/httio) +[![License](https://img.shields.io/npm/l/httio)](https://github.com/vladstsk/httio/blob/main/LICENSE) > Lightweight, type-safe HTTP client for browsers and Node.js. > Built on top of the native `fetch` but provides a better DX with strong typing, middleware and a minimalistic API. @@ -12,31 +14,68 @@ ## Table of Contents 1. [Why Httio?](#why-httio) -2. [Installation](#installation) -3. [Quick Start](#quick-start) -4. [Type-Safe Requests](#type-safe-requests) -5. [Client Configuration](#client-configuration) -6. [Handling Responses](#handling-responses) -7. [Middleware](#middleware) -8. [Error Handling](#error-handling) -9. [Recipes](#recipes) -10. [API Reference](#api-reference) -11. [FAQ](#faq) -12. [License](#license) +2. [Httio vs. The Rest](#httio-vs-the-rest) +3. [Installation](#installation) +4. [Quick Start](#quick-start) + - [ES Modules (`import`)](#es-modules-import) + - [CommonJS (`require`)](#commonjs-require) +5. [Type-Safe Requests](#type-safe-requests) +6. [Client Configuration](#client-configuration) + - [Base client](#base-client) + - [Extending an existing client](#extending-an-existing-client) + - [Supported options](#supported-options) +7. [Handling Responses](#handling-responses) +8. [Middleware](#middleware) +9. [Error Handling](#error-handling) +10. [Recipes](#recipes) + - [Passing query params](#passing-query-params) + - [Uploading files](#uploading-files) + - [Working with streams](#working-with-streams) + - [Reusing an `AbortController`](#reusing-an-abortcontroller) +11. [API Reference](#api-reference) + - [`client(options?)`](#clientoptions) + - [HttioClient` instance](#httioclient-instance) + - [`RequestOptions`](#requestoptions) +12. [FAQ](#faq) +13. [License](#license) --- ## Why Httio? -| Feature | Description | -|---------------------------|-------------------------------------------------------------------------------------------------------| -| **TypeScript-first** | All public types are exported; strict compile-time checks. | -| **Tiny footprint** | Ships as ESM + CJS, zero runtime dependencies. | -| **Lazy parsing** | Body is not parsed automatically—_you_ decide when and how. | +| Feature | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------| +| **TypeScript-first** | All public types are exported; strict compile-time checks. | +| **Tiny footprint** | Ships as ESM + CJS, zero runtime dependencies. | +| **Lazy parsing** | Body is not parsed automatically—_you_ decide when and how. | | **One interface everywhere** | Works in browsers, Node 18+, edge functions—no polyfills required. | -| **Extensible** | Middleware chain for logging, auth, caching, etc. | -| **Convenient cloning** | `extends()` lets you reuse and override base options elegantly. | -| **Full control** | Everything from `fetch` is exposed plus syntactic sugar (`params`, `json`, `timeout`). | +| **Extensible** | Middleware chain for logging, auth, caching, etc. | +| **Convenient cloning** | `extends()` lets you reuse and override base options elegantly. | +| **Full control** | Everything from `fetch` is exposed plus syntactic sugar (`params`, `json`, `timeout`). | + +--- + +## Httio vs. The Rest + +| Library | Size
(min + gzip) | TS‑first | Browser | Node | Native `fetch` | Middleware | Retries | Deps | Notes | +|------------------|:--------------------------------------------------------------------------:|:--------------:|:-------:|:----:|:-------------------:|:----------:|:-------:|:--------------------------------------------------------------------------------------------------------------------------------------:|-------------------------------| +| **Httio** | ![size](https://img.shields.io/bundlephobia/minzip/httio?label) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | [![deps](https://badgen.net/bundlephobia/dependency-count/httio?label)](https://bundlephobia.com/package/httio) | Modern, tiny | +| Axios | ![size](https://img.shields.io/bundlephobia/minzip/axios?label) | ⚠️ | ✅ | ✅ | ❌ | ⚠️ | ⚠️ | [![deps](https://badgen.net/bundlephobia/dependency-count/axios?label)](https://bundlephobia.com/package/axios) | Heavy | +| isomorphic-fetch | ![size](https://img.shields.io/bundlephobia/minzip/isomorphic-fetch?label) | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | [![deps](https://badgen.net/bundlephobia/dependency-count/isomorphic-fetch?label)](https://bundlephobia.com/package/isomorphic-fetch) | Simple shim | +| ky | ![size](https://img.shields.io/bundlephobia/minzip/ky?label) | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | [![deps](https://badgen.net/bundlephobia/dependency-count/ky?label)](https://bundlephobia.com/package/ky) | Small, fetch‑first | +| superagent | ![size](https://img.shields.io/bundlephobia/minzip/superagent?label) | ⚠️ | ✅ | ✅ | ❌ | ✅ | ⚠️ | [![deps](https://badgen.net/bundlephobia/dependency-count/superagent?label)](https://bundlephobia.com/package/superagent) | Classic | +| request | ![size](https://img.shields.io/bundlephobia/minzip/request?label) | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | [![deps](https://badgen.net/bundlephobia/dependency-count/request?label)](https://bundlephobia.com/package/request) | Deprecated | +| r2 | ![size](https://img.shields.io/bundlephobia/minzip/r2?label) | ⚠️ | ❌ | ✅ | ✅ | ❌ | ❌ | [![deps](https://badgen.net/bundlephobia/dependency-count/r2?label)](https://bundlephobia.com/package/r2) | Minimal | +| phin | ![size](https://img.shields.io/bundlephobia/minzip/phin?label) | ⚠️ | ❌ | ✅ | ❌ | ❌ | ✅ | [![deps](https://badgen.net/bundlephobia/dependency-count/phin?label)](https://bundlephobia.com/package/phin) | Promise client | + + +### Key takeaways + +1. **Native by design** – Httio is a thin layer above `fetch`, so knowledge transfers instantly and no shims are required in modern runtimes. +2. **Strict types everywhere** – every public method is generically typed, turning many runtime bugs into compile-time errors. +3. **Zero runtime deps** – smaller bundles, faster cold starts, less supply-chain risk. +4. **Middleware without bloat** – add logging, auth or caching in a few lines, no heavyweight “interceptors” machinery. +5. **One client → any environment** – the same code runs in browsers, Node 18+, edge workers and service workers. --- @@ -60,26 +99,51 @@ For earlier versions add any fetch polyfill. ## Quick Start +### ES Modules (`import`) + ```ts import httio from 'httio'; // GET const users = await httio.get('https://api.example.com/users').json(); +const payload = { + name: 'Alice', + email: 'alice@example.com', +}; + // POST -await httio.post( - 'https://api.example.com/users', - { name: 'Alice', email: 'alice@example.com' } -).json(); +await httio.post('https://api.example.com/users', payload); // PUT with query params -await httio.put( - 'https://api.example.com/users/42', - { name: 'Bob' }, - { params: { notify: true } } -); +await httio.put('https://api.example.com/users/42', payload, { + params: { + notify: true, + }, +}); ``` +### CommonJS (`require`) + +If your project is still on CommonJS (for example when you run Node ≤ 12 or simply prefer `require`), import the library like this: + +```javascript +// Option 1 — named export (recommended) +const { client } = require('httio'); + +// Option 2 — default export (identical to ESM default) +const httio = require('httio'); + +// Example +const api = client({ + url: 'https://api.example.com', +}); + +api.get('/users/me').json().then(console.log); +``` + +`httio` ships with dual ESM/CJS bundles (`index.mjs` & `index.cjs`), so no extra Babel or `type: "module"` setup is required. + --- ## Type-Safe Requests @@ -131,35 +195,46 @@ const api = client({ `extends()` returns a _new_ instance with inherited and/or overridden options: ```ts -const v2 = api.extends({ url: '/v2' }); -await v2.get('/status'); // → https://api.example.com/v2/status +const v2 = api.extends({ + url: '/v2', +}); + +await v2.get('/status'); // → https://api.example.com/v2/status ``` ### Supported options -| Option | Type | Default | Description | -|-----------|----------------------------------------|------------------------------------|-------------------------------------------------| -| `url` | `string \| URL` | – | Base URL for relative paths. | -| `headers` | `HeadersInit` | – | Global headers. | -| `params` | `Record` | – | Query params (auto-encoded). | -| `timeout` | `number` | – | Abort request after N ms via `AbortController`. | -| `url` | `string \| URL` (only in `extends`) | – | Alias for `base` when only host changes. | -| `fetch` | `(input, init) => Promise` | `globalThis.fetch \| window.fetch` | Custom fetch implementation (handy in tests). | +| Option | Type | Default | Description                                    | +|-----------|-----------------------------------------------------------------|-----------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `url` | `string \| URL` | – | Base URL for relative paths. | +| `url` | `string \| URL` (only in `extends`) | – | Extends the base URL or replaces it if the link contains a host. | +| `params` | Record<string, string \| number> | – | Query params (auto-encoded). | +| `timeout` | `number` | `1000` | Abort request after N ms. | +| `retry` | `number \| RetryOptions` | { limit: 3, delay: 1000 } | How many times (and with what delay) to retry failed requests. | +| `fetch` | `Fetcher` | globalThis.fetch \| window.fetch | Custom fetch implementation (handy in tests). | + +> You can also pass any field accepted by the standard Fetch `Request`/`Response` objects; those options are forwarded unchanged. --- ## Handling Responses -Httio returns a **wrapper** around `Response` with extra parsing helpers. -They are _lazy_—HTTP call is sent immediately, but body reading starts only when a parser is invoked. +Httio returns a **wrapper** of type `HttioBody & Promise` around the native `Response`. + +It behaves as both: + +1. `Promise` – you can `await` it or call `.then()`. +2. `HttioBody` – exposes lazy body-parsers: `json()`, `text()`, `blob()`, `buffer()`, `bytes()`, `stream()`. -| Method | Return type | -|------------------|------------------------| -| `json()` | `Promise` | -| `text()` | `Promise` | -| `blob()` | `Promise` | -| `bytes()` | `Promise` | -| `buffer()` | `Promise` | +The HTTP request is dispatched immediately, while the response body is read only when one of the parsers is invoked. + +| Method | Return type | +|------------------|---------------------------| +| `json()` | `Promise` | +| `text()` | `Promise` | +| `blob()` | `Promise` | +| `bytes()` | `Promise` | +| `buffer()` | `Promise` | | `stream()` | `Promise` | Example of deferred parsing: @@ -179,23 +254,23 @@ const data = await response.json(); ## Middleware -Middleware are async functions `(req, next) => Response` executed in a chain: +Middleware are async functions `(request: HttioRequest, next: NextMiddleware): MaybePromise` executed in a chain: ```ts import httio, { type Middleware } from 'httio'; const auth: Middleware = async (req, next) => { req.headers.set('Authorization', `Bearer ${getToken()}`); - + return next(req); }; const logger: Middleware = async (req, next) => { const started = performance.now(); - const res = await next(req); + console.log(`${req.method} ${req.url} → ${res.status} (${Date.now() - started} ms)`); - + return res; }; @@ -213,34 +288,6 @@ HTTP status `4xx/5xx` triggers a `HttpError`: ```ts import httio, { HttpError } from 'httio'; -try { - await httio.get('/admin').json(); -} catch (err) { - if (err instanceof HttpError) { - console.error(`Error ${err.status}: ${err.message}`); - } else { - throw err; // non-HTTP error - } -} -``` - -`HttpError` exposes: - -| Property | Description | -|-------------|----------------------------------| -| `status` | HTTP status code | -| `statusText`| Human-readable text (if any) | -| `response` | Original `Response` object | -| `url` | Final URL (after redirects) | - ---- - -## Error Handling - -HTTP status codes in the `4xx/5xx` range throw an instance of `HttpError`: -```ts -import httio, { HttpError } from 'httio'; - try { await httio.get('/admin').json(); } catch (err) { @@ -257,14 +304,14 @@ try { } ``` -`HttpError` exposes only two properties: +`HttpError` extends the native `Error` class and additionally exposes: -| Property | Type | Description | -|-----------|-----------------|------------------------------------------| -| `request` | `HttioRequest` | Object representing the original request | -| `response`| `HttioResponse` | Object representing the server response | +| Property | Type | Description | +|------------|-----------------|---------------------------------------------------------------| +| `request` | `HttioRequest` | The request object that triggered the error (post-middleware) | +| `response` | `HttioResponse` | The received response (contains status, headers, etc.) | -You can read any additional details (status code, headers, body, etc.) from the `response` object itself. +Standard `Error` fields (`name`, `message`, `stack`) remain available. --- @@ -297,11 +344,11 @@ const reader = stream.getReader(); while (true) { const { value, done } = await reader.read(); - + if (done) { break; } - + console.log(new TextDecoder().decode(value)); } ``` @@ -327,20 +374,21 @@ See [configuration](#supported-options) for the list of parameters. ### `HttioClient` instance -| Method | Description | -|-------------------------------------|------------------------------------------------| +| Method | Description | +|-------------------------------------|----------------------------------------------------------------------| | `get(url, opts?)` | `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` — same signature | -| `extends(opts)` | Returns a new client inheriting options | -| `use(...middleware)` | Adds middleware | +| `extends(opts)` | Returns a new client inheriting options | +| `use(...middleware)` | Adds middleware | ### `RequestOptions` Extends the native `RequestInit`: -| Extra field | Type | Description | -|-------------|---------------------------------|--------------------------------| -| `params` | `Record` | Adds query parameters | -| `timeout` | `number` | Aborts the request after N ms | +| Extra field | Type | Description | +|-------------|-----------------------------------------------------------------|----------------------------------------------------------------| +| `params` | Record<string, string \| number> | Adds query parameters | +| `timeout` | `number` | Aborts the request after N ms | +| `retry` | `number \| RetryOptions` | How many times (and with what delay) to retry failed requests. | ### Types @@ -373,7 +421,10 @@ import { client } from 'httio'; import { createFetchMock } from '@mswjs/interceptors'; const fetchMock = createFetchMock(); -const api = client({ fetch: fetchMock }); + +const api = client({ + fetch: fetchMock, +}); ``` ---