Skip to content

sadinho/httpClient

Repository files navigation

HTTP Client

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

Why Not Axios?

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

Why HTTP Client?

  • 🎯 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

Axios Comparison

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

Migrating from Axios

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');

Features

  • ✨ 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

Installation

npm install httprest-client

or

yarn add httprest-client

or

pnpm add httprest-client

Quick Start

Basic Usage (Axios-style)

import { 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');

In React

// 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>
  );
}

Interceptors (Axios-compatible)

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);
  }
);

Advanced Configuration

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
});

Configuration

Request Config

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;
}

Interceptors

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);
  }
);

Error Handling

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);
  }
}

File Transfer

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);

Development

Install dependencies

npm install

Build

npm run build

Watch mode

npm run build:watch

Storybook

npm run storybook

Configuration

Request Config

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;
}

Error Handling

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);
  }
}

File Transfer

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);

Comparison with Axios

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)

Migrating from Axios

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');

Publishing to NPM

Option 1: Publish from CLI

  1. Configure package.json:

    {
     "name": "httprest-client",
      "version": "1.0.0",
     "author": "Sadinho",
     "repository": { "type": "git", "url": "git+https://github.com/sadinho/httpClient.git" }
    }
  2. Create an npm account:

    npm adduser
  3. Build and test:

    npm run build
    npm test
  4. Publish:

    npm publish

Option 2: Automatic Publishing with GitHub Actions

The repository includes GitHub Actions workflow for automatic publishing.

  1. Generate npm token:

  2. Add GitHub secret:

    • Go to repository Settings → Secrets and variables → Actions
    • Create secret named NPM_TOKEN
    • Paste your npm token
  3. Update package.json:

    {
     "name": "httprest-client"
    }
  4. Push to main branch:

    git push origin main

    → Automatic tests and publish! 🚀

See PUBLISHING.md for detailed instructions.

Development

Install dependencies

npm install

Build

npm run build

Watch mode

npm run build:watch

Storybook

npm run storybook
# Open http://localhost:6006

Tests

npm test

Run tests with UI

npm run test:ui

Lint

npm run lint

Contributing

We welcome contributions! See CONTRIBUTING.md for guidelines.

Real-World Examples

API Service with Auth

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'
);

React Hook for Data Fetching

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 };
}

License

MIT

About

Projeto para substituir uso do axios

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors