Lightweight, type-safe HTTP client for browsers and Node.js.
Built on top of the nativefetchbut provides a better DX with strong typing, middleware and a minimalistic API.
- Why Httio?
- Httio vs. The Rest
- Installation
- Quick Start
- Type-Safe Requests
- Client Configuration
- Handling Responses
- Middleware
- Error Handling
- Recipes
- API Reference
- FAQ
- License
| 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). |
- Native by design – Httio is a thin layer above
fetch, so knowledge transfers instantly and no shims are required in modern runtimes. - Strict types everywhere – every public method is generically typed, turning many runtime bugs into compile-time errors.
- Zero runtime deps – smaller bundles, faster cold starts, less supply-chain risk.
- Middleware without bloat – add logging, auth or caching in a few lines, no heavyweight “interceptors” machinery.
- One client → any environment – the same code runs in browsers, Node 18+, edge workers and service workers.
# npm npm i httio # yarn yarn add httio # pnpm pnpm add httio
Node 18+ already includes fetch.
For earlier versions add any fetch polyfill.
import httio from 'httio'; // GET const users = await httio.get('https://api.example.com/users').json(); const payload = { name: 'Alice', email: '[email protected]', }; // POST await httio.post('https://api.example.com/users', payload); // PUT with query params await httio.put('https://api.example.com/users/42', payload, { params: { notify: true, }, });
If your project is still on CommonJS (for example when you run Node ≤ 12 or simply prefer require), import the library like this:
// 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.
import httio from 'httio'; interface User { id: number; name: string; email: string; } interface CreateUserDto { name: string; email: string; } // Strictly typed array const list = async (): Promise<User[]> => { return httio.get('https://api.example.com/users').json<User[]>(); } // Typed payload + response const create = async (payload: CreateUserDto): Promise<User> => { return httio.post('https://api.example.com/users', payload).json<User>(); };
json<T>() guarantees the shape of data at compile-time.
import { client } from 'httio'; const api = client({ url: 'https://api.example.com', headers: { 'Accept': 'application/json' }, });
extends() returns a new instance with inherited and/or overridden options:
const v2 = api.extends({ url: '/v2', }); await v2.get('/status'); // → https://api.example.com/v2/status
| 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/Responseobjects; those options are forwarded unchanged.
Httio returns a wrapper of type HttioBody & Promise<HttioResponse> around the native Response.
It behaves as both:
Promise<HttioResponse>– you canawaitit or call.then().HttioBody– exposes lazy body-parsers:json(),text(),blob(),buffer(),bytes(),stream().
The HTTP request is dispatched immediately, while the response body is read only when one of the parsers is invoked.
| Method | Return type |
|---|---|
json<T>() |
Promise<T> |
text() |
Promise<string> |
blob() |
Promise<Blob> |
bytes() |
Promise<Uint8Array> |
buffer() |
Promise<ArrayBuffer> |
stream() |
Promise<ReadableStream> |
Example of deferred parsing:
// Fire the request const response = httio.get('/slow-endpoint'); // Do something else in parallel await doSomething(); // Now read the body const data = await response.json<MyData>();
Middleware are async functions (request: HttioRequest, next: NextMiddleware): MaybePromise<HttioResponse | Payload | Response> executed in a chain:
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; }; httio.use(auth, logger);
Flow: auth → logger → fetch → logger → auth.
HTTP status 4xx/5xx triggers a HttpError:
import httio, { HttpError } from 'httio'; try { await httio.get('/admin').json(); } catch (err) { if (err instanceof HttpError) { // Access the original request and response objects const { request, response } = err; console.error( `Request to ${request.url} failed with status ${response.status} ${response.statusText}`, ); } else { throw err; // non-HTTP error } }
HttpError extends the native Error class and additionally exposes:
| Property | Type | Description |
|---|---|---|
request |
HttioRequest |
The request object that triggered the error (post-middleware) |
response |
HttioResponse |
The received response (contains status, headers, etc.) |
Standard Error fields (name, message, stack) remain available.
httio.get('/search', { params: { q: 'httio', page: 2 } });
Produces /search?q=httio&page=2.
const form = new FormData(); form.append('avatar', file); await httio.post('/me/avatar', form);
Content-Type: multipart/form-data is set automatically.
const stream = await httio.get('/logs').stream(); const reader = stream.getReader(); while (true) { const { value, done } = await reader.read(); if (done) { break; } console.log(new TextDecoder().decode(value)); }
const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); // 5 s timeout httio.get('/long', { signal: controller.signal });
Creates a new client.
See configuration for the list of parameters.
| 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 the native RequestInit:
| 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. |
All core interfaces are exported from the package root:
import type { HttioClient, Middleware, HttpError } from 'httio';
Httio focuses on type safety and minimalism.
If you need request/response transformers, cancellation, works-everywhere support and do not want heavy dependencies—yes, Httio can be a solid alternative.
Any environment with fetch (or a polyfill) and ReadableStream works.
IE 11 would require polyfills for both Promise and fetch, but the package is not officially tested there.
Pass your own fetch to the client:
import { client } from 'httio'; import { createFetchMock } from '@mswjs/interceptors'; const fetchMock = createFetchMock(); const api = client({ fetch: fetchMock, });
Distributed under the MIT License.
See LICENSE for more details.