A lightweight, apisauce-inspired HTTP client built on native fetch API with TypeScript support.
- Built on native
fetchAPI (no axios dependency) - Standardized response format with error classification
- Request/Response transforms (sync & async)
- Response monitors for logging/analytics
- Timeout support using AbortController
- Full TypeScript support with generics
- Similar API to apisauce for easy migration
Install via npm:
npm install @mshindi-labs/refetchOr with yarn:
yarn add @mshindi-labs/refetchOr with pnpm:
pnpm add @mshindi-labs/refetchimport { create, PROBLEM_CODE } from '@mshindi-labs/refetch';
// Create an API instance
const api = create({
baseURL: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
},
timeout: 10000, // 10 seconds
});
// Make a GET request
const response = await api.get<User>('/users/1');
if (response.ok) {
console.log('User:', response.data);
} else {
console.error('Error:', response.problem);
// response.problem will be one of:
// - PROBLEM_CODE.CLIENT_ERROR (400-499)
// - PROBLEM_CODE.SERVER_ERROR (500-599)
// - PROBLEM_CODE.TIMEOUT_ERROR
// - PROBLEM_CODE.NETWORK_ERROR
// - PROBLEM_CODE.CONNECTION_ERROR
// - PROBLEM_CODE.CANCEL_ERROR
// - PROBLEM_CODE.UNKNOWN_ERROR
}interface CreateUserData {
name: string;
email: string;
}
const response = await api.post<User>('/users', {
name: 'John Doe',
email: 'john@example.com',
});
if (response.ok) {
console.log('Created user:', response.data);
} else {
console.error('Failed to create user:', response.problem);
}// GET with query params
const users = await api.get('/users', { page: 1, limit: 10 });
// POST with data
const created = await api.post('/users', userData);
// PUT with data
const updated = await api.put('/users/1', userData);
// PATCH with partial data
const patched = await api.patch('/users/1', { name: 'New Name' });
// DELETE with params
const deleted = await api.delete('/users/1');
// HEAD request
const head = await api.head('/users/1');
// LINK request (for linking resources)
const linked = await api.link('/images/avatar.jpg', {}, {
headers: { Link: '<http://example.com/profiles/user>; rel="tag"' }
});
// UNLINK request
const unlinked = await api.unlink('/images/avatar.jpg', {}, {
headers: { Link: '<http://example.com/profiles/user>; rel="tag"' }
});
// Generic method for any HTTP verb (including custom methods)
const response = await api.any({
method: 'PROPFIND',
url: '/webdav/folder',
headers: { Depth: '1' }
});Add authorization headers or modify requests before sending:
// Synchronous transform
api.addRequestTransform((request) => {
const token = localStorage.getItem('token');
if (token) {
request.headers = {
...request.headers,
Authorization: `Bearer ${token}`,
};
}
});
// Async transform
api.addRequestTransform(async (request) => {
const token = await getAuthToken();
request.headers = {
...request.headers,
Authorization: `Bearer ${token}`,
};
});Modify responses after receiving:
api.addResponseTransform((response) => {
// Transform data format
if (response.ok && response.data) {
response.data = transformData(response.data);
}
});
// Async response transform
api.addResponseTransform(async (response) => {
if (response.data) {
response.data = await processData(response.data);
}
});Observe all API responses for logging, analytics, or debugging:
// Simple logging monitor
api.addMonitor((response) => {
console.log('API Response:', {
url: response.response?.url,
status: response.status,
duration: response.duration,
problem: response.problem,
});
});
// Error tracking monitor
api.addMonitor((response) => {
if (!response.ok) {
trackError({
type: response.problem,
error: response.originalError,
status: response.status,
});
}
});
// Performance monitoring
api.addMonitor((response) => {
if (response.duration && response.duration > 3000) {
console.warn('Slow API call:', response.response?.url, response.duration);
}
});// Set a single header
api.setHeader('X-Custom-Header', 'value');
// Set multiple headers
api.setHeaders({
'X-API-Key': 'your-api-key',
'X-Client-Version': '1.0.0',
});
// Delete a header
api.deleteHeader('X-Custom-Header');
// Update base URL
api.setBaseURL('https://api-v2.example.com');
// Get current base URL
const currentBaseURL = api.getBaseURL();
console.log('Current API base:', currentBaseURL);Override instance configuration for specific requests:
const response = await api.get<User>('/users/1', undefined, {
timeout: 5000, // Override timeout for this request
headers: {
'X-Request-Id': crypto.randomUUID(),
},
});All responses follow a consistent format:
interface ApiResponse<T> {
ok: boolean; // true if 200-299
problem: PROBLEM_CODE | null; // Error classification
originalError: Error | null; // Original error if any
data?: T; // Response data
status?: number; // HTTP status code
headers?: Record<string, string>; // Response headers
duration?: number; // Request duration in ms
response?: Response; // Original fetch Response
}export enum PROBLEM_CODE {
NONE = 'NONE', // 200-299
CLIENT_ERROR = 'CLIENT_ERROR', // 400-499
SERVER_ERROR = 'SERVER_ERROR', // 500-599
TIMEOUT_ERROR = 'TIMEOUT_ERROR', // Request timeout
CONNECTION_ERROR = 'CONNECTION_ERROR', // Cannot connect
NETWORK_ERROR = 'NETWORK_ERROR', // Network unavailable
CANCEL_ERROR = 'CANCEL_ERROR', // Request cancelled
UNKNOWN_ERROR = 'UNKNOWN_ERROR', // Unknown error
}// Simple check
if (!response.ok) {
console.error('Request failed:', response.problem);
}
// Specific error handling
switch (response.problem) {
case PROBLEM_CODE.CLIENT_ERROR:
console.error('Client error (400-499):', response.status);
break;
case PROBLEM_CODE.SERVER_ERROR:
console.error('Server error (500-599):', response.status);
break;
case PROBLEM_CODE.TIMEOUT_ERROR:
console.error('Request timed out');
break;
case PROBLEM_CODE.NETWORK_ERROR:
console.error('Network unavailable');
break;
default:
console.error('Unknown error');
}
// Type guards
if (response.ok) {
// TypeScript knows response.data is defined here
console.log(response.data.id);
}Perfect integration with TanStack Query:
import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '@/lib/api'; // Your refetch instance
// Query
function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: async () => {
const response = await api.get<User>(`/users/${id}`);
if (!response.ok) {
throw new Error(response.problem || 'Failed to fetch user');
}
return response.data;
},
});
}
// Mutation
function useCreateUser() {
return useMutation({
mutationFn: async (data: CreateUserData) => {
const response = await api.post<User>('/users', data);
if (!response.ok) {
throw new Error(response.problem || 'Failed to create user');
}
return response.data;
},
});
}- Tiny Bundle Size: Only ~2.3 KB gzipped (no axios dependency)
- Modern Standards: Built on native
fetchAPI with AbortController - Zero Runtime Dependencies: No external dependencies in production
- Full TypeScript Support: Complete type safety with generics
- Flexible Transforms: Add/remove/clear request and response transforms
- Error Classification: Automatic categorization of errors (network, timeout, server, etc.)
- Node.js 18+ Ready: Works in modern Node.js environments
- Browser Compatible: Works in all modern browsers with native fetch support
import type {
ApiResponse,
ApiOkResponse,
ApiErrorResponse,
RefetchInstance,
RefetchConfig,
RequestConfig,
RequestTransform,
AsyncRequestTransform,
ResponseTransform,
AsyncResponseTransform,
Monitor,
} from '@mshindi-labs/refetch';- Create a single instance per API and reuse it:
// lib/api.ts
import { create } from '@mshindi-labs/refetch';
export const api = create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});- Use transforms for common logic:
// Add auth token to all requests
api.addRequestTransform((request) => {
const token = getToken();
if (token) {
request.headers = { ...request.headers, Authorization: `Bearer ${token}` };
}
});- Handle errors consistently:
api.addMonitor((response) => {
if (!response.ok && response.problem === PROBLEM_CODE.CLIENT_ERROR) {
if (response.status === 401) {
// Handle unauthorized
redirectToLogin();
}
}
});- Type your responses:
interface User {
id: string;
name: string;
email: string;
}
const response = await api.get<User>('/users/1');
// response.data is typed as User when ok is trueNew HTTP Methods:
// LINK and UNLINK methods for resource linking
await api.link('/resources/image.jpg', {}, { headers: { Link: '...' }});
await api.unlink('/resources/image.jpg', {}, { headers: { Link: '...' }});
// Generic any() method for custom HTTP verbs
await api.any({ method: 'PROPFIND', url: '/webdav', headers: { Depth: '1' }});Base URL Getter:
// Get current base URL
const baseURL = api.getBaseURL();Optimizations:
- Reduced bundle size to ~2.3 KB gzipped (40% smaller)
- Simplified header management with normalized Headers class
- Removed code duplication and unused constants
- Better tree-shaking support
Bug Fixes:
- Fixed DEFAULT_HEADERS being applied to FormData and URLSearchParams (file uploads now work correctly)
- Consistent error response handling across all error paths
- Improved error classification
Type Safety Improvements:
- All
anytypes replaced withunknownfor better type safety - You must now explicitly type your responses or provide type assertions
Config Protection:
api.configis now readonly and returns a copy- Use setter methods like
setBaseURL()instead of direct mutation
Transform & Monitor Management:
// Remove specific transforms/monitors
const transform = (config) => { /* ... */ };
api.addRequestTransform(transform);
api.removeRequestTransform(transform);
// Clear all transforms/monitors
api.clearRequestTransforms();
api.clearResponseTransforms();
api.clearMonitors();Type Guards:
import { isOkResponse, isErrorResponse } from '@mshindi-labs/refetch';
const response = await api.get<User>('/users/1');
if (isOkResponse(response)) {
// TypeScript knows response.data is User
console.log(response.data.name);
}
if (isErrorResponse(response)) {
// TypeScript knows this is an error
console.log(response.problem);
}Better Error Messages:
- Errors now include HTTP method and full URL
- Example:
HTTP GET https://api.example.com/users failed with status 404: Not Found
Improved URL Handling:
- Properly handles absolute URLs
- Normalizes trailing/leading slashes
- Better baseURL concatenation
// v1.x - implicit any types
const response = await api.get('/users');
// v2.x - explicit typing required
const response = await api.get<User[]>('/users');
// v1.x - direct config mutation
api.config.timeout = 5000;
// v2.x - use setter methods
// (config is now readonly)Contributions are welcome! Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.
MIT License - see LICENSE file for details.