A powerful, type-safe HTTP client for JavaScript/TypeScript with support for interceptors, authentication, file uploads, and framework-agnostic storage.
- đź”’ Automatic Authentication - Built-in token management with customizable storage
- 🔌 Interceptors - Request, response, and error interceptors for all instances
- 📦 Type-Safe - Full TypeScript support with generic types
- 📤 File Upload - Multi-file upload with progress tracking
- 🎯 URL Interpolation - Dynamic URL parameters with template strings
- 🏠Factory Pattern - Generate type-safe API client functions
- 🔄 Framework Agnostic - Works with any storage implementation
- 🚀 Zero Dependencies - Only peer dependency on
@mxweb/utils
npm install @mxweb/http @mxweb/utils
# or
yarn add @mxweb/http @mxweb/utils
# or
pnpm add @mxweb/http @mxweb/utilsimport { Http } from "@mxweb/http";
// Create an instance
const http = new Http("https://api.example.com");
// Make a GET request
const { data, success, error } = await http.get<User[]>("/users");
if (success) {
console.log("Users:", data);
} else {
console.error("Error:", error);
}// Simple GET
const response = await http.get<User>("/users/123");
// GET with query parameters
const response = await http.get<User[]>("/users", {
page: 1,
limit: 10,
status: "active",
});
// GET with additional options
const response = await http.get<User[]>("/users", query, {
headers: { "X-Custom-Header": "value" },
params: { organizationId: "456" },
signal: abortController.signal,
});// Create a new user
const response = await http.post<User>("/users", {
name: "John Doe",
email: "john@example.com",
});
// With additional options
const response = await http.post<User>("/users", userData, {
headers: { "X-Custom-Header": "value" },
params: { organizationId: "456" },
});const response = await http.put<User>("/users/123", {
name: "Jane Doe",
email: "jane@example.com",
});const response = await http.patch<User>("/users/123", {
email: "newemail@example.com",
});const response = await http.delete<void>("/users/123");const response = await http.head("/users/123");const response = await http.options("/users/123");const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const response = await http.upload<UploadResponse>("/upload", file, {
name: "document",
onProgress: (progress) => {
console.log(`Upload progress: ${progress.percentage}%`);
console.log(`Loaded: ${progress.loaded} / ${progress.total} bytes`);
},
});// Using File array
const files = Array.from(fileInput.files);
await http.upload("/upload", files, {
name: "files[]",
body: {
userId: 123,
category: "documents",
},
});
// Using FileList
await http.upload("/upload", fileInput.files, {
name: "attachments",
onProgress: (progress) => {
updateProgressBar(progress.percentage);
},
});Use template strings in URLs with the params option:
// URL: /users/{userId}/posts/{postId}
const response = await http.get<Post>("/users/{userId}/posts/{postId}", null, {
params: {
userId: 123,
postId: 456,
},
});
// Actual request: /users/123/posts/456Modify requests before they are sent:
// Instance-level interceptor
http.on("request", async (options) => {
console.log("Making request to:", options.url);
// Add custom headers
options.headers = {
...options.headers,
"X-Request-Time": Date.now().toString(),
};
return options;
});
// Static interceptor (applies to all instances)
Http.on("request", async (options) => {
// Apply to all Http instances
return options;
});Process responses before they are returned:
http.on("response", async (response) => {
console.log("Response status:", response.status);
// Log or transform response
if (!response.ok) {
console.error("Request failed:", response.statusText);
}
return response;
});Handle errors globally:
http.on("error", async (error) => {
console.error("Request error:", error);
// Send to error tracking service
errorTracker.log(error);
// Show notification to user
showErrorNotification("Request failed. Please try again.");
});const requestInterceptor = async (options) => {
// ...
return options;
};
http.on("request", requestInterceptor);
// Later, remove it
http.off("request", requestInterceptor);The Http client automatically manages authentication tokens:
// Set up storage (localStorage example)
http.setStorage(localStorage);
// Token is automatically retrieved and added to requests
// Default header: Authorization: Bearer {token}
const response = await http.get("/protected-resource");// Using static properties
Http.authTokenKey = "access_token"; // Storage key
Http.authHeaderKey = "Authorization"; // Header name
Http.authHeaderType = "Bearer"; // Token prefix
// Or using environment variables
// API_AUTH_TOKEN_KEY=access_token
// API_AUTH_HEADER_KEY=Authorization
// API_AUTH_HEADER_TYPE=Bearerimport { HttpStorage } from "@mxweb/http";
class CookieStorage implements HttpStorage {
getItem(key: string): string | null {
const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
return match ? match[1] : null;
}
setItem(key: string, value: string): void {
document.cookie = `${key}=${value}; path=/`;
}
removeItem(key: string): void {
document.cookie = `${key}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`;
}
}
http.setStorage(new CookieStorage());Create type-safe API client functions from endpoint definitions:
import { Http, HttpMethod } from "@mxweb/http";
// Define your endpoints
const endpoints = {
"user.list": "/api/users",
"user.get": "/api/users/{id}",
"user.create": "/api/users",
"user.update": "/api/users/{id}",
"user.delete": "/api/users/{id}",
"post.list": "/api/posts",
"post.upload": "/api/posts/{id}/attachments",
};
// Create factory function
const factory = Http.createFactory({
baseURL: "https://api.example.com",
endpoint: endpoints,
storage: localStorage, // or async function
});
// Generate API functions
const getUsers = factory<User[]>("user.list", HttpMethod.GET);
const getUser = factory<User, [{ id: string }]>("user.get", HttpMethod.GET);
const createUser = factory<User, [CreateUserInput]>("user.create", HttpMethod.POST);
const updateUser = factory<User, [UpdateUserInput]>("user.update", HttpMethod.PUT);
const deleteUser = factory<void>("user.delete", HttpMethod.DELETE);
const uploadAttachment = factory<UploadResponse, [File, HttpUploadOptions]>(
"post.upload",
"UPLOAD"
);
// Use the generated functions
const { data: users } = await getUsers.fn();
const { data: user } = await getUser.fn({ id: "123" });
const { data: newUser } = await createUser.fn({
name: "John Doe",
email: "john@example.com",
});
// Upload with factory
const file = document.querySelector("input").files[0];
const { data: upload } = await uploadAttachment.fn(file, {
name: "attachment",
params: { id: "123" },
onProgress: (p) => console.log(`${p.percentage}%`),
});const endpoints = {
api: {
users: {
list: "/users",
get: "/users/{id}",
},
posts: {
list: "/posts",
create: "/posts",
},
},
};
// The factory flattens nested objects with dot notation
const factory = Http.createFactory({ endpoint: endpoints });
const getUsers = factory<User[]>("api.users.list", HttpMethod.GET);
const getPosts = factory<Post[]>("api.posts.list", HttpMethod.GET);const factory = Http.createFactory({
baseURL: "https://api.example.com",
endpoint: endpoints,
storage: async () => {
// Lazy load storage
const { default: storage } = await import("./storage");
return storage;
},
});// Add headers to specific instance
http.addHeaders({
"X-API-Version": "v1",
"X-Client-Id": "web-app",
});// Add headers for one request only
Http.extraHeaders = {
"X-Request-ID": generateRequestId(),
};
await http.get("/users"); // Includes X-Request-ID
await http.get("/posts"); // Does NOT include X-Request-IDAll requests return a standardized response:
interface HttpResponse<T = unknown, E = unknown> {
success: boolean; // true if status 200-299
data: T; // Response data
status: number; // HTTP status code
statusText: string; // HTTP status text
headers: Record<string, string>; // Response headers
error: E | null; // Error data (if success is false)
}const { success, data, error, status, headers } = await http.get<User>("/users/123");
if (success) {
console.log("User:", data);
console.log("Content-Type:", headers["content-type"]);
} else {
console.error("Error:", error);
console.log("Status:", status);
}await http.get("/users", {
page: 1,
limit: 10,
status: "active",
sort: "name",
});
// Request: /users?page=1&limit=10&status=active&sort=nameawait http.get("/users", [
["page", 1],
["limit", 10],
["tags", "javascript"],
["tags", "typescript"],
]);const params = new URLSearchParams();
params.append("page", "1");
params.append("limit", "10");
await http.get("/users", params);await http.get("/users", "page=1&limit=10&status=active");const formData = new FormData();
formData.append("name", "John Doe");
formData.append("email", "john@example.com");
// Content-Type is automatically set to multipart/form-data
const response = await http.post<User>("/users", formData);const controller = new AbortController();
// Start request
const requestPromise = http.get("/users", null, {
signal: controller.signal,
});
// Cancel after 5 seconds
setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await requestPromise;
} catch (error) {
if (error.name === "AbortError") {
console.log("Request was cancelled");
}
}The Http client supports these environment variables:
API_URL- Base URL for all requests (default:/)API_AUTH_TOKEN_KEY- Storage key for auth token (default:access_token)API_AUTH_HEADER_KEY- Header name for auth token (default:Authorization)API_AUTH_HEADER_TYPE- Token prefix (default:Bearer)
Full TypeScript support with generics:
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserInput {
name: string;
email: string;
}
interface ApiError {
message: string;
code: string;
}
// Type-safe request
const response = await http.post<User, CreateUserInput, ApiError>("/users", {
name: "John",
email: "john@example.com",
});
// TypeScript knows the types
if (response.success) {
console.log(response.data.name); // string
} else {
console.log(response.error?.message); // string | undefined
}constructor(baseURL?: string)Http.method- HttpMethod enumHttp.authTokenKey- Token storage key (default:'access_token')Http.authHeaderKey- Auth header name (default:'Authorization')Http.authHeaderType- Token prefix (default:'Bearer')Http.authDetectToken- Storage detection arrayHttp.extraHeaders- One-time headersHttp.storage- Global storage instance
Http.on(...params)- Register global interceptorHttp.off(...params)- Remove global interceptorHttp.createFactory(options)- Create factory function
setStorage(storage)- Set storage implementationaddHeaders(headers)- Add default headerson(...params)- Register instance interceptoroff(...params)- Remove instance interceptorrequest(options)- Make HTTP requestget(url, query?, options?)- GET requestpost(url, body?, options?)- POST requestput(url, body?, options?)- PUT requestpatch(url, body?, options?)- PATCH requestdelete(url, options?)- DELETE requesthead(url, options?)- HEAD requestoptions(url, options?)- OPTIONS requestupload(url, file, options?)- Upload file(s)
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
PATCH = "PATCH",
HEAD = "HEAD",
OPTIONS = "OPTIONS",
}import { Http } from "@mxweb/http";
import { useState, useEffect } from "react";
const http = new Http("https://api.example.com");
function useUsers() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function fetchUsers() {
const { success, data, error } = await http.get<User[]>("/users");
if (success) {
setUsers(data);
} else {
setError(error as Error);
}
setLoading(false);
}
fetchUsers();
}, []);
return { users, loading, error };
}import { Http } from "@mxweb/http";
import { ref } from "vue";
const http = new Http("https://api.example.com");
export function useUsers() {
const users = ref<User[]>([]);
const loading = ref(true);
const error = ref<Error | null>(null);
async function fetchUsers() {
loading.value = true;
const { success, data, error: err } = await http.get<User[]>("/users");
if (success) {
users.value = data;
} else {
error.value = err as Error;
}
loading.value = false;
}
fetchUsers();
return { users, loading, error, refetch: fetchUsers };
}async function fetchWithRetry<T>(
requestFn: () => Promise<HttpResponse<T>>,
maxRetries = 3
): Promise<HttpResponse<T>> {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await requestFn();
if (response.success) return response;
lastError = response;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
throw lastError;
}
// Usage
const response = await fetchWithRetry(() => http.get<User[]>("/users"));MIT
Contributions are welcome! Please feel free to submit a Pull Request.