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 httioNode 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.