httix-http
v1.0.5
Published
Ultra-lightweight, type-safe, zero-dependency HTTP client built on native Fetch — modern axios replacement.
Maintainers
Readme
Why httix-http?
| Feature | httix-http | axios | got | ky | ofetch | |---|---|---|---|---|---| | Dependencies | 0 | 2 | 11 | 2 | 5 | | Size (min+gzip) | ~5 kB | ~28 kB | ~67 kB | ~9 kB | ~12 kB | | Built on Fetch API | ✅ | ❌ | ❌ | ✅ | ✅ | | TypeScript native | ✅ | ⚠️ (v1 types) | ✅ | ✅ | ✅ | | Interceptors | ✅ | ✅ | ✅ | ❌ | ✅ | | Retry with backoff | ✅ | ❌ (plugin) | ✅ | ✅ | ✅ | | Request deduplication | ✅ | ❌ | ❌ | ❌ | ✅ | | Rate limiting | ✅ | ❌ | ❌ | ❌ | ❌ | | Middleware pipeline | ✅ | ❌ | ❌ | ❌ | ❌ | | Auth helpers | ✅ | ❌ (plugin) | ✅ | ❌ | ❌ | | Auto-pagination | ✅ | ❌ | ✅ | ❌ | ❌ | | SSE / NDJSON streaming | ✅ | ❌ | ✅ | ❌ | ❌ | | Cache plugin | ✅ | ❌ (adapter) | ✅ | ✅ | ✅ | | Mock plugin (testing) | ✅ | ✅ (adapter) | ✅ | ❌ | ❌ | | Response timing | ✅ | ❌ | ✅ | ❌ | ❌ | | Cancel all requests | ✅ | ⚠️ (manual) | ✅ | ✅ | ❌ | | ESM + CJS | ✅ | ✅ | ❌ (ESM) | ✅ | ✅ | | Runtime agnostic | ✅ | ✅ | Node only | Browser | Universal |
Installation
# npm
npm install httix-http
# yarn
yarn add httix-http
# pnpm
pnpm add httix-http
# bun
bun add httix-httpQuick Start
1. Simple GET request
import httix from 'httix-http';
const { data, status, timing } = await httix.get('/users');
console.log(data); // parsed JSON response
console.log(status); // 200
console.log(timing); // request duration in ms2. POST with JSON body
import httix from 'httix-http';
const { data } = await httix.post('/users', {
name: 'Avinash',
email: '[email protected]',
});
console.log(data.id); // created user id3. Create a configured client
import { createHttix } from 'httix-http';
const api = createHttix({
baseURL: 'https://api.example.com',
auth: { type: 'bearer', token: 'my-secret-token' },
headers: { 'X-App-Version': '1.0.0' },
});
const { data } = await api.get('/users/me');4. Error handling
import httix, { HttixResponseError, HttixTimeoutError, HttixAbortError } from 'httix-http';
try {
const { data } = await httix.get('/users/999');
} catch (error) {
if (error instanceof HttixResponseError) {
console.error(`Server error: ${error.status} — ${error.statusText}`);
console.error('Response body:', error.data);
} else if (error instanceof HttixTimeoutError) {
console.error(`Request timed out after ${error.timeout}ms`);
} else if (error instanceof HttixAbortError) {
console.error('Request was cancelled');
}
}5. Streaming SSE events
import httix from 'httix-http';
for await (const event of httix.stream.sse('/events')) {
console.log(`[${event.type}] ${event.data}`);
if (event.type === 'done') break;
}API Reference
Creating Instances
createHttix(config?)
Create a new client instance with the given configuration. This is the recommended entry-point for creating dedicated API clients.
import { createHttix } from 'httix-http';
const api = createHttix({
baseURL: 'https://api.example.com/v2',
headers: {
'X-App-Version': '1.0.0',
'Accept-Language': 'en-US',
},
timeout: 15000,
retry: { attempts: 5, backoff: 'exponential' },
auth: { type: 'bearer', token: 'my-token' },
});
const { data } = await api.get('/users');httix.create(config?)
Create a derived client from the default instance, merging new configuration with the defaults:
import httix from 'httix-http';
const adminApi = httix.create({
baseURL: 'https://admin.api.example.com',
auth: { type: 'bearer', token: adminToken },
});Default instance
A pre-configured default instance is exported for convenience:
import httix from 'httix-http';
// Use directly
await httix.get('/users');
// Destructure
const { get, post, put, patch, delete: remove } = httix;
await get('/users');HTTP Methods
All methods return Promise<HttixResponse<T>> and support a generic type parameter for the response body.
httix.get<T>(url, config?)
const users = await httix.get<User[]>('/users');
console.log(users.data); // User[]
// With query parameters
const page = await httix.get<User[]>('/users', {
query: { page: 1, limit: 20, active: true },
});httix.post<T>(url, body?, config?)
const user = await httix.post<User>('/users', {
name: 'Jane',
email: '[email protected]',
});
// With FormData
const form = new FormData();
form.append('avatar', fileInput.files[0]);
const upload = await httix.post('/upload', form);httix.put<T>(url, body?, config?)
const updated = await httix.put<User>('/users/1', {
name: 'Jane Updated',
email: '[email protected]',
});httix.patch<T>(url, body?, config?)
const patched = await httix.patch<User>('/users/1', { name: 'Jane v2' });httix.delete<T>(url, config?)
const result = await httix.delete<{ deleted: boolean }>('/users/1');
console.log(result.data.deleted); // truehttix.head(url, config?)
const headers = await httix.head('/large-file.pdf');
console.log(headers.headers.get('content-length')); // "1048576"httix.options(url, config?)
const allowed = await httix.options('/api');
console.log(allowed.headers.get('allow')); // "GET, POST, OPTIONS"httix.request<T>(config)
The underlying method for all HTTP shortcuts. Use it for maximum control:
const { data } = await httix.request<User>({
method: 'POST',
url: '/users',
body: { name: 'Jane' },
headers: { 'X-Custom-Header': 'value' },
timeout: 5000,
retry: { attempts: 2 },
query: { verify: true },
responseType: 'json',
});The Response Object
Every method returns an HttixResponse<T>:
interface HttixResponse<T> {
data: T; // Parsed response body
status: number; // HTTP status code (e.g. 200)
statusText: string; // HTTP status text (e.g. "OK")
headers: Headers; // Native Headers object
ok: boolean; // true if status is 2xx
raw: Response; // Original Fetch Response
timing: number; // Request duration in ms
config: HttixRequestConfig; // Config that produced this response
}const response = await httix.get('/users');
console.log(response.data); // parsed body
console.log(response.status); // 200
console.log(response.ok); // true
console.log(response.timing); // 142 (ms)
console.log(response.headers.get('x-ratelimit-remaining')); // "99"Interceptors
Interceptors let you run logic before a request is sent or after a response is received. They are identical in concept to axios interceptors.
Request Interceptor
// Add a request ID and timestamp to every outgoing request
httix.interceptors.request.use((config) => {
config.headers = config.headers ?? {};
if (config.headers instanceof Headers) {
config.headers.set('X-Request-ID', crypto.randomUUID());
} else {
config.headers['X-Request-ID'] = crypto.randomUUID();
}
return config;
});Response Interceptor
// Transform response data
httix.interceptors.response.use((response) => {
// Wrap data in an envelope
response.data = { success: true, data: response.data };
return response;
});Response Error Interceptor
// Handle 401 globally — attempt token refresh
httix.interceptors.response.use(
(response) => response,
(error) => {
if (error instanceof HttixResponseError && error.status === 401) {
console.error('Unauthorized — redirecting to login');
window.location.href = '/login';
}
// Return void to let the error propagate
return;
},
);Ejecting Interceptors
const id = httix.interceptors.request.use((config) => {
config.headers = config.headers ?? {};
if (config.headers instanceof Headers) {
config.headers.set('X-Trace', 'enabled');
} else {
config.headers['X-Trace'] = 'enabled';
}
return config;
});
// Remove the interceptor later
httix.interceptors.request.eject(id);
// Clear all interceptors
httix.interceptors.request.clear();Retry Configuration
Automatic retry with configurable backoff strategies is built-in and enabled by default.
import { createHttix } from 'httix-http';
const client = createHttix({
baseURL: 'https://api.example.com',
retry: {
attempts: 5, // Max retry attempts (default: 3)
backoff: 'exponential', // 'fixed' | 'linear' | 'exponential'
baseDelay: 1000, // Base delay in ms (default: 1000)
maxDelay: 30000, // Max delay cap in ms (default: 30000)
jitter: true, // Add randomness to prevent thundering herd (default: true)
retryOn: [408, 429, 500, 502, 503, 504], // Status codes to retry (default)
retryOnNetworkError: true, // Retry on DNS/network failures (default: true)
retryOnSafeMethodsOnly: false, // Only retry GET/HEAD/OPTIONS (default: false)
retryCondition: (error) => { // Custom retry condition
// Don't retry if the response contains a specific error code
if (error instanceof HttixResponseError && error.data?.code === 'NO_RETRY') {
return false;
}
return true;
},
onRetry: (attempt, error, delay) => { // Callback before each retry
console.warn(`Retry attempt ${attempt} after ${delay}ms — ${error.message}`);
},
},
});Disable retry for a single request:
const { data } = await httix.get('/ephemeral', { retry: false });Timeout & Abort
Timeout
Every request has a default 30-second timeout. Override per-request or globally:
// Per-request timeout
const { data } = await httix.get('/slow-endpoint', { timeout: 5000 });
// Global timeout
const client = createHttix({ timeout: 10000 });Abort with AbortController
Cancel individual requests using a standard AbortController:
const controller = new AbortController();
// Cancel after 2 seconds
setTimeout(() => controller.abort(), 2000);
try {
const { data } = await httix.get('/large-dataset', {
signal: controller.signal,
});
} catch (error) {
if (httix.isCancel(error)) {
console.log('Request was cancelled by the user');
}
}Cancel all in-flight requests
// Cancel every pending request on this client
httix.cancelAll('User navigated away');
// Check if an error is from cancellation
try {
await httix.get('/data');
} catch (error) {
if (httix.isCancel(error)) {
console.log(error.reason); // "User navigated away"
}
}Streaming
Server-Sent Events (SSE)
Stream SSE events as an async iterable:
import httix from 'httix-http';
for await (const event of httix.stream.sse('https://api.example.com/events', {
headers: { 'Accept': 'text/event-stream' },
})) {
console.log(`[Event: ${event.type}]`, event.data);
if (event.id) {
console.log(`Last event ID: ${event.id}`);
}
if (event.type === 'shutdown') break;
}NDJSON Streaming
Stream newline-delimited JSON objects:
import httix from 'httix-http';
interface LogEntry {
timestamp: string;
level: string;
message: string;
}
for await (const entry of httix.stream.ndjson<LogEntry>('/logs/stream')) {
console.log(`[${entry.level}] ${entry.message}`);
}Request Deduplication
Automatically deduplicate identical in-flight requests. When enabled, if multiple calls are made with the same config before the first resolves, they share the same promise.
import { createHttix } from 'httix-http';
const client = createHttix({
baseURL: 'https://api.example.com',
dedup: true,
});
// Both calls will share the same underlying request
const [users1, users2] = await Promise.all([
client.get('/users'),
client.get('/users'),
]);
// Advanced configuration
const client2 = createHttix({
dedup: {
enabled: true,
ttl: 60000, // Cache dedup result for 60s
generateKey: (config) => `${config.method}:${config.url}`,
},
});Rate Limiting
Client-side rate limiting to avoid overwhelming APIs:
import { createHttix } from 'httix-http';
const client = createHttix({
baseURL: 'https://rate-limited-api.example.com',
rateLimit: {
maxRequests: 10, // Max 10 requests
interval: 1000, // Per 1 second window
},
});
// Requests will be automatically throttled
const results = await Promise.all([
client.get('/resource/1'),
client.get('/resource/2'),
client.get('/resource/3'),
// ... up to 10 concurrent, rest queued
]);Middleware
Middleware functions have access to both the request and response, and can modify either:
import httix, { type MiddlewareFn, type MiddlewareContext } from 'httix-http';
// Timing middleware
const timingMiddleware: MiddlewareFn = async (ctx, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`[Timing] ${ctx.request.method} ${ctx.request.url} — ${duration}ms`);
};
// Request/response logging middleware
const loggingMiddleware: MiddlewareFn = async (ctx, next) => {
console.log(`>> ${ctx.request.method} ${ctx.request.url}`);
await next();
if (ctx.response) {
console.log(`<< ${ctx.response.status} ${ctx.response.statusText}`);
}
};
// Register middleware
httix.use(timingMiddleware);
httix.use(loggingMiddleware);
// Middleware is also configurable at client creation
const client = httix.create({
middleware: [timingMiddleware, loggingMiddleware],
});Auth
Bearer Auth
import { createHttix } from 'httix-http';
// Static token
const client = createHttix({
baseURL: 'https://api.example.com',
auth: { type: 'bearer', token: 'my-jwt-token' },
});
// Dynamic token (e.g., from a store)
const client2 = createHttix({
baseURL: 'https://api.example.com',
auth: {
type: 'bearer',
token: () => localStorage.getItem('access_token') ?? '',
refreshToken: async () => {
const res = await fetch('/auth/refresh', { method: 'POST' });
const { accessToken } = await res.json();
localStorage.setItem('access_token', accessToken);
return accessToken;
},
onTokenRefresh: (token) => {
localStorage.setItem('access_token', token);
},
},
});Basic Auth
const client = createHttix({
baseURL: 'https://api.example.com',
auth: {
type: 'basic',
username: 'admin',
password: 'secret',
},
});API Key Auth
// API key in header
const client = createHttix({
baseURL: 'https://api.example.com',
auth: {
type: 'apiKey',
key: 'X-API-Key',
value: 'my-api-key',
in: 'header',
},
});
// API key in query string
const client2 = createHttix({
baseURL: 'https://api.example.com',
auth: {
type: 'apiKey',
key: 'api_key',
value: 'my-api-key',
in: 'query',
},
});Pagination
Automatically fetch all pages of a paginated resource:
import { createHttix } from 'httix-http';
const client = createHttix({ baseURL: 'https://api.example.com' });Offset-based pagination
for await (const page of client.paginate<User>('/users', {
pagination: {
style: 'offset',
pageSize: 50,
offsetParam: 'offset',
limitParam: 'limit',
maxPages: 20, // safety limit
},
})) {
console.log(`Fetched ${page.length} users`);
// process page...
}Cursor-based pagination
interface CursorResponse {
items: User[];
next_cursor: string | null;
}
for await (const page of client.paginate<CursorResponse>('/users', {
pagination: {
style: 'cursor',
pageSize: 100,
cursorParam: 'cursor',
cursorExtractor: (data) => data.next_cursor,
dataExtractor: (data) => data.items,
stopCondition: (data) => data.next_cursor === null,
},
})) {
console.log(`Batch: ${page.length} users`);
}Link header pagination (GitHub-style)
for await (const page of client.paginate<Repo[]>('/repos', {
pagination: {
style: 'link',
},
})) {
console.log(`Fetched ${page.length} repos`);
}Query & Path Parameters
Query Parameters
// Simple query object
const { data } = await httix.get('/search', {
query: {
q: 'typescript',
page: 1,
limit: 20,
sort: 'stars',
order: 'desc',
},
});
// => GET /search?q=typescript&page=1&limit=20&sort=stars&order=desc
// Array values
const { data: filtered } = await httix.get('/items', {
query: {
tags: ['javascript', 'http', 'fetch'],
},
});
// => GET /items?tags=javascript&tags=http&tags=fetch
// Null/undefined values are automatically filtered
const { data: clean } = await httix.get('/items', {
query: {
q: 'search',
page: null, // omitted
debug: undefined, // omitted
},
});
// => GET /items?q=searchPath Parameters
Use :paramName syntax in the URL and provide values via the params option:
const { data } = await httix.get('/users/:userId/posts/:postId', {
params: { userId: '42', postId: '100' },
});
// => GET /users/42/posts/100
// Numbers are automatically converted to strings
const { data: repo } = await httix.get('/repos/:owner/:repo', {
params: { owner: 'Avinashvelu03', repo: 'httix' },
});
// => GET /repos/Avinashvelu03/httixPlugins
Plugins extend httix by registering interceptors and lifecycle hooks. Import them from httix/plugins.
import { loggerPlugin, cachePlugin, mockPlugin } from 'httix-http/plugins';
import { createHttix } from 'httix-http';
const client = createHttix({ baseURL: 'https://api.example.com' });
// Install a plugin
const logger = loggerPlugin({ level: 'debug' });
// The plugin's install() is called, which registers interceptorsCache Plugin
LRU response cache with configurable TTL, stale-while-revalidate, and size limits:
import { cachePlugin } from 'httix-http/plugins';
import { createHttix } from 'httix-http';
const client = createHttix({ baseURL: 'https://api.example.com' });
const cache = cachePlugin({
maxSize: 200, // Max 200 entries (default: 100)
ttl: 5 * 60 * 1000, // 5 minute TTL (default: 300000)
staleWhileRevalidate: true, // Serve stale data while revalidating
swrWindow: 60 * 1000, // 1 minute SWR window (default: 60000)
methods: ['GET'], // Only cache GET requests
respectCacheControl: true, // Respect server Cache-Control headers
});
// Manually manage the cache
cache.invalidate('/users'); // Invalidate a specific key
cache.invalidatePattern(/^\/users\//); // Invalidate by regex pattern
cache.clear(); // Clear entire cache
console.log(cache.getStats()); // { size: 12, maxSize: 200, ttl: 300000 }Logger Plugin
Structured logging of request/response lifecycle events:
import { loggerPlugin } from 'httix-http/plugins';
import { createHttix } from 'httix-http';
const client = createHttix({ baseURL: 'https://api.example.com' });
loggerPlugin({
level: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'none'
logRequestBody: true, // Log request body (default: false)
logResponseBody: true, // Log response body (default: false)
logRequestHeaders: true, // Log request headers (default: false)
logResponseHeaders: true, // Log response headers (default: false)
logger: { // Custom logger (default: console)
debug: (...args) => myLogger.debug(args),
info: (...args) => myLogger.info(args),
warn: (...args) => myLogger.warn(args),
error: (...args) => myLogger.error(args),
},
});Mock Plugin
Replace fetch with an in-memory mock adapter — perfect for unit tests:
import { mockPlugin } from 'httix-http/plugins';
import { createHttix } from 'httix-http';
const mock = mockPlugin();
const client = createHttix({ baseURL: 'https://api.example.com' });
// Register mock handlers (fluent API)
mock
.onGet('/users')
.reply(200, [
{ id: 1, name: 'Jane' },
{ id: 2, name: 'John' },
])
.onGet(/\/users\/\d+/)
.reply(200, { id: 1, name: 'Jane' })
.onPost('/users')
.reply(201, { id: 3, name: 'Created' })
.onDelete(/\/users\/\d+/)
.reply(204, null);
// Use the client normally — requests hit the mock
const { data } = await client.get('/users');
console.log(data); // [{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }]
// Inspect request history
const history = mock.getHistory();
console.log(history.get.length); // 1
console.log(history.get[0].method); // "GET"
console.log(history.get[0].url); // "https://api.example.com/users"
// Reset handlers and history (adapter stays active)
mock.adapter.reset();
// Fully deactivate and restore the original fetch
mock.restore();With a test framework (e.g., Vitest):
import { describe, it, expect, afterEach } from 'vitest';
import { mockPlugin } from 'httix-http/plugins';
import { createHttix } from 'httix-http';
describe('Users API', () => {
const mock = mockPlugin();
const client = createHttix({ baseURL: 'https://api.example.com' });
afterEach(() => {
mock.restore();
});
it('fetches all users', async () => {
mock.onGet('/users').reply(200, [{ id: 1, name: 'Jane' }]);
const { data, status } = await client.get('/users');
expect(status).toBe(200);
expect(data).toEqual([{ id: 1, name: 'Jane' }]);
});
it('creates a user', async () => {
mock.onPost('/users').reply(201, { id: 2, name: 'John' });
const { data, status } = await client.post('/users', { name: 'John' });
expect(status).toBe(201);
expect(data.name).toBe('John');
const history = mock.getHistory();
expect(history.post).toHaveLength(1);
expect(history.post[0].body).toEqual({ name: 'John' });
});
});Error Handling
httix provides a structured error hierarchy. All errors extend HttixError.
| Error Class | When it's thrown | Key properties |
|---|---|---|
| HttixError | Base for all httix errors | config, cause |
| HttixRequestError | Network failure (DNS, CORS, etc.) | message |
| HttixResponseError | Server returns 4xx or 5xx | status, statusText, data, headers |
| HttixTimeoutError | Request exceeds timeout | timeout |
| HttixAbortError | Request is cancelled | reason |
| HttixRetryError | All retry attempts exhausted | attempts, lastError |
import {
HttixError,
HttixRequestError,
HttixResponseError,
HttixTimeoutError,
HttixAbortError,
HttixRetryError,
} from 'httix-http';
try {
await httix.get('/unstable-endpoint');
} catch (error) {
if (error instanceof HttixResponseError) {
// 4xx or 5xx — the response body is available
console.error(`${error.status} ${error.statusText}:`, error.data);
if (error.status === 429) {
console.error('Rate limited — slow down!');
const retryAfter = error.headers?.get('retry-after');
console.log(`Retry after: ${retryAfter}s`);
}
if (error.status >= 500) {
console.error('Server error — this is not your fault');
}
} else if (error instanceof HttixTimeoutError) {
console.error(`Timed out after ${error.timeout}ms`);
} else if (error instanceof HttixRetryError) {
console.error(`Failed after ${error.attempts} attempts`);
console.error('Last error:', error.lastError.message);
} else if (error instanceof HttixRequestError) {
console.error('Network error:', error.message);
console.error('Original cause:', error.cause?.message);
} else if (error instanceof HttixAbortError) {
console.error('Cancelled:', error.reason);
} else if (error instanceof HttixError) {
// Catch-all for any other httix error
console.error('Httix error:', error.message);
console.error('Request config:', error.config?.url);
}
}Disabling throw on non-2xx
If you prefer to handle status codes yourself instead of relying on exceptions:
const response = await httix.get('/users/999', { throwOnError: false });
if (response.ok) {
console.log(response.data);
} else {
console.error(`Error: ${response.status} — ${response.statusText}`);
console.error(response.data); // still accessible
}TypeScript Usage
httix is written in TypeScript and provides first-class type support.
Generic response typing
interface User {
id: number;
name: string;
email: string;
}
// Type the response data
const { data } = await httix.get<User[]>('/users');
// data is User[]
const { data: user } = await httix.post<User>('/users', { name: 'Jane' });
// user is UserTyping request config
import type { HttixRequestConfig, HttixResponse, RetryConfig } from 'httix-http';
const config: HttixRequestConfig = {
url: '/users',
method: 'GET',
query: { page: 1 },
timeout: 10000,
retry: {
attempts: 3,
backoff: 'exponential',
} satisfies RetryConfig,
};Typing middleware
import type { MiddlewareFn, MiddlewareContext, HttixResponse } from 'httix-http';
const myMiddleware: MiddlewareFn<User, HttixRequestConfig, HttixResponse<User>> = async (
ctx: MiddlewareContext<HttixRequestConfig, HttixResponse<User>>,
next,
) => {
// ctx.request is typed as HttixRequestConfig
// ctx.response is typed as HttixResponse<User> | undefined
await next();
if (ctx.response) {
ctx.response.data; // User
}
};Typing plugins
import type { HttixPlugin } from 'httix-http';
const myPlugin: HttixPlugin = {
name: 'my-plugin',
install(client) {
client.interceptors.request.use((config) => config);
},
cleanup() {
// cleanup logic
},
};Migration from axios
Migrating from axios to httix is straightforward. Here are the key differences:
Import changes
// axios
import axios from 'axios';
const { data } = await axios.get('/users');
// httix
import httix from 'httix-http';
const { data } = await httix.get('/users');Instance creation
// axios
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
});
// httix
const api = createHttix({
baseURL: 'https://api.example.com',
timeout: 10000,
});POST requests
// axios — body is the second argument
const { data } = await axios.post('/users', { name: 'Jane' });
// httix — same API
const { data } = await httix.post('/users', { name: 'Jane' });Interceptors
// axios
axios.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
// httix — same pattern
httix.interceptors.request.use((config) => {
config.headers = config.headers ?? {};
if (config.headers instanceof Headers) {
config.headers.set('Authorization', `Bearer ${token}`);
} else {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});Error handling
// axios
try {
await axios.get('/users');
} catch (error) {
if (axios.isAxiosError(error)) {
console.log(error.response?.status);
console.log(error.response?.data);
}
}
// httix
try {
await httix.get('/users');
} catch (error) {
if (error instanceof HttixResponseError) {
console.log(error.status);
console.log(error.data);
}
}Key API differences
| Feature | axios | httix |
|---|---|---|
| Cancel token | new axios.CancelToken() | AbortController |
| Response data | response.data | response.data ✅ (same) |
| Response status | response.status | response.status ✅ (same) |
| Request timeout | timeout: 5000 | timeout: 5000 ✅ (same) |
| Config merge | shallow merge | deep merge |
| params (query) | params: { a: 1 } | query: { a: 1 } |
| Path params | manual | params: { id: 1 } with :id in URL |
| Auto retry | needs plugin | built-in |
| Dedup | not available | built-in |
| Rate limiting | not available | built-in |
| Middleware | not available | built-in |
Benchmarks
Performance measured on Node.js 22 (V8) against a local test server, averaged over 10,000 iterations.
| Operation | httix | axios | ky | node-fetch | |---|---|---|---|---| | Simple GET (cold) | 0.08 ms | 0.42 ms | 0.12 ms | 0.09 ms | | Simple GET (warm) | 0.04 ms | 0.38 ms | 0.08 ms | 0.06 ms | | POST with JSON | 0.09 ms | 0.45 ms | 0.14 ms | 0.11 ms | | With retry (3x) | 0.15 ms | — | 0.19 ms | — | | With interceptors | 0.06 ms | 0.52 ms | — | — | | Dedup hit | 0.01 ms | — | — | — | | Bundle size (min) | 5.1 kB | 27.8 kB | 8.9 kB | 12.4 kB | | Bundle size (gzip) | 2.3 kB | 13.1 kB | 4.2 kB | 5.7 kB |
Note: Benchmarks are synthetic and measure the client-side overhead (request construction, config merging, interceptor execution). Actual network latency dominates real-world timings. Run
npm run benchmarkto reproduce on your machine.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines on setting up the development environment, coding standards, and the PR process.
License
MIT © 2025 Avinashvelu03
