A lightweight Axios-inspired HTTP client built on top of the native Fetch API.
Modern Node.js and browsers already ship with fetch. This library keeps the Axios-style developer experience (instances, defaults, interceptors, and error shape) without adding an HTTP dependency.
- Axios-like API:
get,post,put,patch,delete,head,options, andrequest - Callable instance style:
api('/users', config) - Request and response interceptors
- Config defaults with
create(...) - Typed responses with TypeScript generics
- Native fetch under the hood
- Node.js 18+
- Modern browsers with native fetch support
- TypeScript projects (declaration file included)
npm install# Type-check and build
npm run build
# Run sample entrypoint
npm run start
# Run in watch mode
npm run devCurrent npm scripts in package.json point to src/index.ts for start and dev.
This project uses Vitest for unit testing and MSW (Mock Service Worker) for mocking HTTP requests.
# Watch mode - re-runs on file changes
npm run test
# Single run (useful for CI/CD)
npm run test:run
# Visual test runner UI
npm run test:ui
# Generate coverage report
npm run test:coverageThe test suite includes 27 comprehensive tests covering:
- All HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
- Request and response interceptors
- Error handling and HTTP status codes
- Headers management and merging
- Instance creation and isolation
- Query parameters and timeout configuration
All tests run against mocked HTTP endpoints, ensuring fast and reliable execution without external dependencies.
import httpClient, { HttpClientError, HttpStatusCode } from './src/http-client';
const api = httpClient.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000,
headers: {
'X-Custom-Header': 'MyClient',
},
});
api.interceptors.request.use((config) => {
config.headers = config.headers || {};
config.headers.Authorization = 'Bearer TOKEN_123';
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => Promise.reject(error),
);
async function run() {
try {
const post = await api.get('/posts/1');
console.log(post.data);
const comments = await api('/comments', {
method: 'GET',
params: { postId: 1 },
});
console.log(comments.data);
const created = await api.request('/posts', {
method: 'POST',
data: {
title: 'New Post',
body: 'Post content sent via httpClient',
userId: 1,
},
});
console.log(created.status === HttpStatusCode.Created);
} catch (error: any) {
if (error instanceof HttpClientError || error?.isHttpClientError) {
console.error(error.message, error.code, error.response?.status);
return;
}
console.error(error);
}
}
run();const api = httpClient.create({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'X-App': 'demo',
},
});| Method | Signature |
|---|---|
api(config) |
(config: HttpClientRequestConfig) => Promise<HttpClientResponse> |
api(url, config) |
(url: string, config?: HttpClientRequestConfig) => Promise<HttpClientResponse> |
api.request(...) |
Same as callable signatures |
api.get(url, config?) |
Promise<HttpClientResponse<T>> |
api.delete(url, config?) |
Promise<HttpClientResponse<T>> |
api.head(url, config?) |
Promise<HttpClientResponse<T>> |
api.options(url, config?) |
Promise<HttpClientResponse<T>> |
api.post(url, data?, config?) |
Promise<HttpClientResponse<T>> |
api.put(url, data?, config?) |
Promise<HttpClientResponse<T>> |
api.patch(url, data?, config?) |
Promise<HttpClientResponse<T>> |
const id = api.interceptors.request.use(
(config) => config,
(error) => Promise.reject(error),
);
api.interceptors.request.eject(id);interface User {
id: number;
name: string;
}
const response = await api.get<User>('/users/1');
console.log(response.data.name);The client throws HttpClientError for:
- HTTP non-2xx responses (
ERR_BAD_RESPONSE) - Network failures (
ERR_NETWORK) - Timeout (
ECONNABORTED) - Abort/cancel (
ERR_CANCELED)
For non-2xx responses, error.response contains:
datastatusstatusTextheadersconfig
- Uses native fetch from the runtime.
- Timeout is implemented via
AbortController. - GET and HEAD requests ignore request body.
- JSON payloads are stringified automatically and
Content-Type: application/jsonis set when not provided.