A drop-in replacement for Axios with TypeScript support, interceptors, retry logic, and timeout handling — without the axios dependency.
Migration ready for teams replacing Axios.
A modern HTTP client for React and Node.js teams that want to replace Axios with a smaller, dependency-free, Fetch-based alternative.
Security note: this project was created in response to recent security concerns and failures reported around Axios. The goal is to provide a modern, smaller, dependency-free alternative for teams that want to move away from axios in production.
Português Brasileiro | English
Axios is a solid library, but many teams now want a smaller client with no production dependencies, a simpler runtime profile, and a cleaner security posture. HTTP Client is built for that migration path.
- Replace Axios with minimal code changes
- Remove a production dependency from your stack
- Use a Fetch-based transport layer
- Keep interceptors, timeout, retry, and file helpers
- Adopt a client created specifically after recent Axios security concerns
- 🎯 Replace Axios: Drop-in replacement for Axios without changing your code patterns
- 🚀 Lightweight: ~15KB gzipped, zero production dependencies
- 📝 Fully Typed: Complete TypeScript support
- 🔄 Interceptors: Request and response interceptors (same pattern as Axios)
- ⏱️ Timeout: Built-in timeout support
- 🔁 Retry Logic: Automatic retry with exponential backoff
- 📦 File Transfer: Utilities for downloading and handling files
- 🎯 Flexible: Support for all HTTP methods and response types
- ⚡ Fast: Built on modern Fetch API
| Capability | HTTP Client | Axios |
|---|---|---|
| Drop-in API replacement | Yes | Yes |
| Production dependencies | Zero | Yes |
| Bundled size | Smaller | Larger |
| TypeScript support | Native | Native |
| Interceptors | Yes | Yes |
| Retry support | Built-in | Requires extra setup |
| Timeout handling | Built-in | Yes |
| File transfer helpers | Yes | Limited |
| Transport layer | Fetch API | Custom adapter layer |
| Security posture | Designed as a lean alternative | Depends on upstream package lifecycle |
The API is intentionally familiar so most Axios code can be moved with minimal changes.
| Axios | HTTP Client |
|---|---|
axios.create({ baseURL }) |
new HttpClient({ baseURL }) |
axios.get('/users') |
api.get('/users') |
axios.post('/users', data) |
api.post('/users', data) |
axios.interceptors.request.use(...) |
api.interceptors.request.use(...) |
axios.interceptors.response.use(...) |
api.interceptors.response.use(...) |
// Axios
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com'
});
const { data } = await api.get('/users');// HTTP Client
import { HttpClient } from 'httprest-client';
const api = new HttpClient({
baseURL: 'https://api.example.com'
});
const { data } = await api.get('/users');- ✨ Same interface as Axios — no learning curve
- 🔒 Type-safe with TypeScript
- 📦 Works with React, Vue, Next.js, and Node.js
- 🎨 Easy to customize and extend
- 🧪 Fully tested with 100% coverage
npm install httprest-clientor
yarn add httprest-clientor
pnpm add httprest-clientimport { HttpClient } from 'httprest-client';
// Create an instance
const api = new HttpClient({
baseURL: 'https://api.example.com'
});
// GET request
const { data } = await api.get('/users');
// GET with params
const response = await api.get('/search', {
params: { q: 'typescript', limit: 10 }
});
// POST request
const newUser = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});
// PUT/PATCH/DELETE
await api.put('/users/1', { name: 'Jane Doe' });
await api.patch('/users/1', { status: 'active' });
await api.delete('/users/1');// hooks/useApi.ts
import { useEffect, useState } from 'react';
import { HttpClient } from 'httprest-client';
const api = new HttpClient({
baseURL: process.env.REACT_APP_API_URL
});
export function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
api.get<T>(url)
.then(res => setData(res.data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// App.tsx
import { useApi } from './hooks/useApi';
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const { data: users, loading } = useApi<User[]>('/users');
if (loading) return <div>Loading...</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}import { HttpClient, HttpClientError } from 'httprest-client';
const api = new HttpClient({
baseURL: 'https://api.example.com'
});
// Request interceptor - add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
};
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle errors
api.interceptors.response.use(
(response) => {
console.log('Response:', response.status);
return response;
},
(error) => {
if (error instanceof HttpClientError) {
if (error.response.status === 401) {
// Handle unauthorized
localStorage.removeItem('authToken');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);import { HttpClient } from 'httprest-client';
const api = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 10000, // 10 seconds
retry: 3, // Retry 3 times
retryDelay: (attempt) => {
// Exponential backoff: 1s, 2s, 4s
return Math.pow(2, attempt) * 1000;
},
retryOnStatuses: [408, 429, 500, 502, 503, 504],
retryOnNetworkError: true,
retryOnTimeout: true,
headers: {
'X-Custom-Header': 'value'
}
});
// Make requests with custom config
const response = await api.request({
method: 'POST',
url: '/upload',
data: formData,
responseType: 'json',
timeout: 30000
});interface RequestConfig {
url?: string;
baseURL?: string;
params?: QueryParams;
data?: unknown;
headers?: HeadersInit;
method?: HttpMethod;
timeout?: number;
retry?: number;
retryDelay?: number | ((attempt: number, error: unknown) => number);
retryOnStatuses?: number[];
retryOnMethods?: HttpMethod[];
retryOnNetworkError?: boolean;
retryOnTimeout?: boolean;
responseType?: HttpResponseType;
}const client = new HttpClient();
// Add request interceptor
client.interceptors.request.use(
(config) => {
// Modify request config
config.headers = { ...config.headers, 'Authorization': 'Bearer token' };
return config;
},
(error) => Promise.reject(error)
);
// Add response interceptor
client.interceptors.response.use(
(response) => response,
(error) => {
// Handle error
console.error(error);
return Promise.reject(error);
}
);try {
const response = await client.get('/data');
} catch (error) {
if (error instanceof HttpClientError) {
console.error('HTTP Error:', error.response.status);
} else if (error instanceof HttpClientTimeoutError) {
console.error('Timeout Error:', error.config.timeout);
}
}import { downloadBlob, extractFilenameFromContentDisposition } from 'httprest-client';
const response = await client.get('/download', { responseType: 'blob' });
const filename = extractFilenameFromContentDisposition(response.headers['content-disposition']);
downloadBlob(response.data, filename);npm installnpm run buildnpm run build:watchnpm run storybookinterface RequestConfig {
url?: string;
baseURL?: string;
params?: QueryParams;
data?: unknown;
headers?: HeadersInit;
method?: HttpMethod;
timeout?: number;
retry?: number;
retryDelay?: number | ((attempt: number, error: unknown) => number);
retryOnStatuses?: number[];
retryOnMethods?: HttpMethod[];
retryOnNetworkError?: boolean;
retryOnTimeout?: boolean;
responseType?: HttpResponseType;
}import { HttpClientError, HttpClientTimeoutError } from 'httprest-client';
try {
const response = await api.get('/data');
} catch (error) {
if (error instanceof HttpClientError) {
console.error('HTTP Error:', error.response.status);
console.error('Response data:', error.response.data);
} else if (error instanceof HttpClientTimeoutError) {
console.error('Timeout after:', error.config.timeout);
} else {
console.error('Unknown error:', error);
}
}import {
downloadBlob,
extractFilenameFromContentDisposition,
getFilenameFromResponseHeaders,
inferExtensionFromMimeType
} from 'httprest-client';
// Download a file
const response = await api.get('/api/document.pdf', {
responseType: 'blob'
});
const filename = getFilenameFromResponseHeaders(response.headers);
downloadBlob(response.data, filename);| Feature | Axios | HTTP Client |
|---|---|---|
| Bundle size | ~13 KB | ~5 KB |
| Dependencies | 1 (follow-redirects) | 0 |
| API compatibility | - | ✅ 100% |
| Interceptors | ✅ Yes | ✅ Yes |
| Retry logic | ❌ No | ✅ Yes |
| Timeout support | ✅ Yes | ✅ Yes |
| TypeScript | ✅ Yes | ✅ Full |
| Response types | ✅ Yes | ✅ Yes |
| Cancel tokens | ✅ Yes | ❌ No* |
*Use AbortController instead (native fetch)
Simply replace your import:
// Before
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.example.com' });
// After
import { HttpClient } from 'httprest-client';
const api = new HttpClient({ baseURL: 'https://api.example.com' });
// Everything else stays the same! ✨
const { data } = await api.get('/users');-
Configure package.json:
{ "name": "httprest-client", "version": "1.0.0", "author": "Sadinho", "repository": { "type": "git", "url": "git+https://github.com/sadinho/httpClient.git" } } -
Create an npm account:
npm adduser
-
Build and test:
npm run build npm test -
Publish:
npm publish
The repository includes GitHub Actions workflow for automatic publishing.
-
Generate npm token:
- Visit: https://www.npmjs.com/settings/~profile/tokens
- Create "Publish" token
- Copy the token
-
Add GitHub secret:
- Go to repository Settings → Secrets and variables → Actions
- Create secret named
NPM_TOKEN - Paste your npm token
-
Update package.json:
{ "name": "httprest-client" } -
Push to main branch:
git push origin main
→ Automatic tests and publish! 🚀
See PUBLISHING.md for detailed instructions.
npm installnpm run buildnpm run build:watchnpm run storybook
# Open http://localhost:6006npm testnpm run test:uinpm run lintWe welcome contributions! See CONTRIBUTING.md for guidelines.
import { HttpClient, HttpClientError } from 'httprest-client';
class ApiService {
private api: HttpClient;
constructor(baseURL: string) {
this.api = new HttpClient({ baseURL });
this.setupInterceptors();
}
private setupInterceptors() {
// Add auth token
this.api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
};
}
return config;
});
// Handle errors globally
this.api.interceptors.response.use(
(response) => response,
(error) => {
if (error instanceof HttpClientError) {
if (error.response.status === 401) {
this.handleUnauthorized();
}
}
throw error;
}
);
}
private handleUnauthorized() {
localStorage.removeItem('token');
window.location.href = '/login';
}
async getUsers() {
return this.api.get('/users');
}
async createUser(data: any) {
return this.api.post('/users', data);
}
}
export const apiService = new ApiService(
process.env.REACT_APP_API_URL || 'https://api.example.com'
);import { useEffect, useState } from 'react';
import { HttpClientError } from 'httprest-client';
import { apiService } from './services/api';
export function useData<T>(endpoint: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
apiService.api.get<T>(endpoint)
.then(response => {
if (isMounted) {
setData(response.data);
setError(null);
}
})
.catch(err => {
if (isMounted) {
const message = err instanceof HttpClientError
? `Error ${err.response.status}`
: 'Network error';
setError(message);
setData(null);
}
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false;
};
}, [endpoint]);
return { data, loading, error };
}MIT