@alvin0/http-driver
v0.2.6
Published
HttpDriver helps manage APIs on a per-service basis with direct Axios and Fetch support (no apisauce).
Maintainers
Readme
HttpDriver
Modern, fully typed per-service HTTP client for Axios and Fetch. Complete TypeScript support with automatic type inference, standardized responses, transform hooks, interceptor support, and AbortController handling across both paths.
Features
- Full TypeScript Support: Complete type inference for all methods after build - no manual type casting needed
- Dual HTTP paths: Axios via execService, Fetch via execServiceByFetch
- Flexible Version Management: Optional dynamic version injection with multiple positioning strategies
- Standard ResponseFormat shape with ok, status, data, headers, duration, problem, originalError
- Builder pattern with full set of hooks (Axios sync/async transforms, error interceptor; Fetch request/response transforms)
- URL template compilation and query assembly with version support
- JSON and multipart/form-data payload shaping (Fetch drops manual Content-Type for multipart)
- AbortController support on both paths (Axios cancellation normalized to TimeoutError)
Overview
HttpDriver is an advanced library designed to streamline API management by organizing interactions on a per-service basis. Leveraging both Axios and the Fetch API, it delivers a robust, Promise-based HTTP client with enhanced error handling, asynchronous transformations, and interceptor support.
At its core, HttpDriver features:
- Dual HTTP method support (Axios and Fetch)
- Customizable request and response transformations (synchronous and asynchronous)
- Robust error handling with interceptors (including token refresh and request queuing)
- Standardized response formatting including execution duration, status, data, and error details
Key Components
- Service: An individual API endpoint definition. Services specify endpoints, HTTP methods, URL patterns with path parameters, and optional default options. Grouping related endpoints into services improves maintainability and comprehension.
- Driver: Central runtime that binds the baseURL and services, exposes execution methods, wires Axios request/response transforms and interceptors, and provides Fetch request/response transform hooks.
- DriverBuilder: Builder pattern to configure and compile your driver with all hooks in a fluent, type-safe manner.
Key Components Diagram
graph TD;
A[Service Definitions] --> B[Driver Configuration];
B --> C[DriverBuilder];
C --> D[Driver HTTP Client];How HttpDriver Works
- Initialization: The Driver constructs an Axios instance based on your configuration, attaches synchronous and asynchronous transforms, and sets up error interceptors for token refresh patterns. For Fetch, request and response transform hooks are available.
- Service Execution:
- execService: Uses Axios to call compiled URLs, handle payloads (including multipart/form-data with FormData), and pass custom headers. Returns a standardized response with ok, status, data, headers, problem, originalError, and duration.
- execServiceByFetch: Uses the Fetch API to invoke compiled URLs, measure duration precisely, parse responses (strict JSON by default), and apply optional request/response transforms.
- Both methods share the same standardized response shape, simplifying error handling and integration in your app.
Three-Step Process to Build a Driver
- Define Services: Specify your API endpoints (id, templated URL, method).
- Register Driver: Provide baseURL and services, optionally configuring transforms and interceptors.
- Build with DriverBuilder: Compile into a Promise-based client and use execService / execServiceByFetch across your app.
Install
npm install @alvin0/http-driver
Quick start
TypeScript Support
HttpDriver provides complete TypeScript support with automatic type inference:
import { DriverBuilder, MethodAPI } from "@alvin0/http-driver";
// After build(), the driver has full type inference
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices([
{ id: "getUser", url: "users/{id}", method: MethodAPI.get }
])
.build(); // Returns HttpDriverInstance & AxiosInstance
// Full type inference - no manual casting needed!
const result = await driver.execService({ id: "getUser", params: { id: "1" } });
// TypeScript knows: result.ok, result.status, result.data, result.headers, etc.
const fetchResult = await driver.execServiceByFetch({ id: "getUser" });
// Same complete type inference
// Driver also provides all Axios methods with types
await driver.get("/direct-endpoint");
await driver.post("/direct-endpoint", { data: "test" });Basic Setup
- Define services
import { MethodAPI, type ServiceApi } from "@alvin0/http-driver";
const services: ServiceApi[] = [
{ id: "posts.list", url: "posts", method: MethodAPI.get },
{ id: "posts.detail", url: "posts/{id}", method: MethodAPI.get },
{ id: "auth.login", url: "login/auth", method: MethodAPI.post },
];- Build a driver
import { DriverBuilder } from "@alvin0/http-driver";
import type { ResponseFormat } from "@alvin0/http-driver";
const httpApi = new DriverBuilder()
.withBaseURL("https://jsonplaceholder.typicode.com")
.withServices(services)
// Axios sync transform
.withAddRequestTransformAxios((req) => {
req.headers = { ...(req.headers || {}), "X-App": "demo" };
})
// Axios async transforms
.withAddAsyncRequestTransformAxios((register) => {
register(async (req) => {
req.headers = { ...(req.headers || {}), "X-Async-Req": "1" };
});
})
.withAddResponseTransformAxios((_resp) => {
// inspect normalized ApiResponse-like
})
.withAddAsyncResponseTransformAxios((register) => {
register(async (_res) => {
// async work
});
})
})
// Axios error interceptor (optional)
.withHandleInterceptorErrorAxios((axiosInstance) => async (error) => {
// e.g., retry once on 401
if (error?.response?.status === 401 && !error?.config?._retry) {
error.config._retry = true;
return axiosInstance.request(error.config);
}
return Promise.reject(error);
})
// Fetch transforms
.withAddRequestTransformFetch((url, requestOptions) => {
const u = new URL(url);
u.searchParams.set("via", "fetch-transform");
return {
url: u.toString(),
requestOptions: {
...requestOptions,
headers: { ...(requestOptions.headers || {}), "X-Fetch": "1" },
},
};
})
.withAddTransformResponseFetch((response: ResponseFormat) => {
return { ...response, data: { wrapped: true, original: response.data } } as ResponseFormat;
})
.build();- Make requests
// Axios path
const list = await httpApi.execService({ id: "posts.list" });
// Fetch path
const listByFetch = await httpApi.execServiceByFetch({ id: "posts.list" });Version Configuration
HttpDriver provides flexible version management for API services with optional version building. By default, version information in service definitions is ignored unless explicitly enabled.
Why Version Configuration?
Before:
- Hardcode version in baseURL:
https://api.example.com/v1 - Difficult to change versions per service
- Inflexible when API provider changes version patterns
After:
- Clean baseURL:
https://api.example.com - Optional automatic version injection with flexible positioning
- Service-level version overrides
- Support for multiple version patterns
Important: Version Building is Optional
⚠️ By default, version information in services is ignored. You must explicitly enable version building to use this feature.
// ❌ Version is IGNORED by default
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices([
{ id: "users", url: "users", method: MethodAPI.get, version: 1 } // This version is ignored!
])
.build();
// Result: https://api.example.com/users (version ignored)
// ✅ Must explicitly enable version building
const driverWithVersions = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices([
{ id: "users", url: "users", method: MethodAPI.get, version: 1 }
])
.enableVersioning() // Enable version building
.build();
// Result: https://api.example.com/v1/usersWays to Enable Version Building
1. Using enableVersioning()
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices(services)
.enableVersioning() // Enable with default settings
.withGlobalVersion(1)
.build();2. Using withVersionTemplate()
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices(services)
.withVersionTemplate("{baseURL}/api/{version}/{endpoint}") // Auto-enables versioning
.build();3. Using withVersionConfig() with enabled: true
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices(services)
.withVersionConfig({
enabled: true, // Explicitly enable
position: 'after-base',
defaultVersion: 1
})
.build();Basic Usage
// Simple global version (must enable first)
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com") // No hardcoded version
.withServices(services)
.enableVersioning() // Enable version building
.withGlobalVersion(1) // Global version for all services
.build();
// Result: https://api.example.com/v1/usersService-specific Version Override
const services: ServiceApi[] = [
{
id: "user.list",
url: "users",
method: MethodAPI.get,
// Uses global version
},
{
id: "user.detail",
url: "users/{id}",
method: MethodAPI.get,
version: 2, // Override global version
},
];
const driver = new DriverBuilder()
.withBaseURL("https://api.example.com")
.withServices(services)
.enableVersioning() // Must enable version building
.withGlobalVersion(1)
.build();
// user.list: https://api.example.com/v1/users
// user.detail: https://api.example.com/v2/users/123Version Positioning Strategies
After Base URL (Default)
.enableVersioning() // Enable version building
.withVersionConfig({
position: 'after-base',
defaultVersion: 1
})
// Result: https://api.example.com/v1/usersBefore Endpoint
.enableVersioning() // Enable version building
.withVersionConfig({
position: 'before-endpoint',
defaultVersion: 1
})
// Result: https://api.example.com/users/v1Subdomain Versioning
.enableVersioning() // Enable version building
.withVersionConfig({
position: 'prefix',
defaultVersion: 1
})
// Result: https://v1.api.example.com/usersCustom Template (Auto-enables versioning)
.withVersionTemplate("{baseURL}/api/{version}/{endpoint}")
.withGlobalVersion(1)
// Result: https://api.example.com/api/v1/usersAdvanced Configuration
// Enterprise API with custom pattern
const driver = new DriverBuilder()
.withBaseURL("https://api.company.com")
.withVersionConfig({
enabled: true, // Explicitly enable
position: 'custom',
template: '{baseURL}/rest/api/{version}/services/{endpoint}',
prefix: 'api-v', // Custom prefix instead of 'v'
defaultVersion: '2024.1'
})
.withServices(services)
.build();
// Result: https://api.company.com/rest/api/api-v2024.1/services/usersVersion Configuration Options
interface VersionConfig {
enabled?: boolean; // Enable/disable version building (default: false)
position?: 'after-base' | 'before-endpoint' | 'prefix' | 'custom';
template?: string; // For custom position: '{baseURL}/api/{version}/{endpoint}'
prefix?: string; // Default: 'v', set to '' for no prefix
defaultVersion?: string | number; // Global default version
}Builder Methods
// Enable version building
.enableVersioning(enabled?: boolean)
// Set version template (auto-enables versioning)
.withVersionTemplate(template: string)
// Set complete version configuration
.withVersionConfig(config: VersionConfig)
// Quick set global version only
.withGlobalVersion(version: string | number)Migration from Previous Versions
If you're upgrading from a previous version where version building was always enabled:
// Before (automatic versioning)
const driver = new DriverBuilder()
.withServices([{ id: 'users', url: 'users', method: 'get', version: 1 }])
.build();
// After (must enable explicitly)
const driver = new DriverBuilder()
.withServices([{ id: 'users', url: 'users', method: 'get', version: 1 }])
.enableVersioning() // Add this line
.build();For detailed examples and migration guide, see Version Configuration Documentation.
Service ID convention (enum recommended)
For maintainability and strong typing, define service IDs as an enum. We recommend a namespaced pattern: v{version}.{domain}.{resource}.{action}
Benefits:
- Type-safe IDs with auto-complete
- Consistent naming across your codebase
- Easier refactors and usage in call sites
Example:
import {
MethodAPI,
type ServiceApi,
} from "@alvin0/http-driver/dist/utils/driver-contracts";
export enum ExampleServiceIds {
List = "v1.example.games.list",
ListCouponAvailable = "v1.example.games.coupon-available",
ListMerchantAvailable = "v1.example.games.merchant-available",
DownloadCSV = "v1.example.games.download-csv",
Store = "v1.example.games.store",
Detail = "v1.example.games.detail",
Update = "v1.example.games.update",
Destroy = "v1.example.games.destroy",
Restore = "v1.example.games.restore",
}
export default [
{
id: ExampleServiceIds.List,
url: "v1/admin/games",
method: MethodAPI.get,
version: 1,
},
{
id: ExampleServiceIds.ListCouponAvailable,
url: "v1/admin/games/coupon-available",
method: MethodAPI.get,
version: 1,
},
{
id: ExampleServiceIds.ListMerchantAvailable,
url: "v1/admin/games/merchant-available",
method: MethodAPI.get,
version: 1,
},
{
id: ExampleServiceIds.DownloadCSV,
url: "v1/admin/games/csv",
method: MethodAPI.get,
version: 1,
},
{
id: ExampleServiceIds.Store,
url: "v1/admin/games",
method: MethodAPI.post,
version: 1,
},
{
id: ExampleServiceIds.Detail,
url: "v1/admin/games/{id}",
method: MethodAPI.get,
version: 1,
},
{
id: ExampleServiceIds.Update,
url: "v1/admin/games/{id}",
method: MethodAPI.put,
version: 1,
},
{
id: ExampleServiceIds.Destroy,
url: "v1/admin/games/{id}",
method: MethodAPI.delete,
version: 1,
},
{
id: ExampleServiceIds.Restore,
url: "v1/admin/games/{id}/restore",
method: MethodAPI.patch,
version: 1,
},
] as ServiceApi[];Usage:
// Axios path
const detail = await httpApi.execService({
id: ExampleServiceIds.Detail,
params: { id: 123 },
});
// Fetch path
const list = await httpApi.execServiceByFetch({ id: ExampleServiceIds.List });AbortController
Axios path (cancel maps to TimeoutError):
const c = new AbortController();
const p = httpApi.execService({ id: "posts.list" }, undefined, { signal: c.signal });
c.abort();
const res = await p; // res.ok === false, problem ~ 'timeout'Fetch path:
const c = new AbortController();
const p = httpApi.execServiceByFetch({ id: "posts.list" }, undefined, { signal: c.signal });
c.abort();
const res = await p; // res.ok === false, problem ~ 'timeout'You may also pass { abortController } and the library will forward .signal.
getInfoURL
Compile a full URL and see how GET payloads become query strings:
const info = httpApi.getInfoURL({ id: "posts.detail", params: { id: 1 } }, { q: "abc", page: 2 });
// info.fullUrl -> https://jsonplaceholder.typicode.com/posts/1?q=abc&page=2Standalone httpClientFetch
import { httpClientFetch } from "@alvin0/http-driver/dist/utils";
import { MethodAPI } from "@alvin0/http-driver/dist/utils/driver-contracts";
const res = await httpClientFetch({ url: "https://example.com/posts/{id}", method: MethodAPI.get, param: { id: "1" } });Response shape
Every call returns:
interface ResponseFormat<T = any> {
ok: boolean;
problem: string | null;
originalError: string | null;
data: T;
status: number;
headers?: Headers | null;
duration: number;
}Error normalization includes TimeoutError, NetworkError, MalformedResponseError; fetch JSON parsing is strict by default.
Multipart (Fetch)
When headers["Content-Type"] === "multipart/form-data", the library removes explicit headers so the browser can set the boundary. Body is built from your payload FormData automatically.
Multiple drivers
import postsServices from "./posts-services";
import adminServices from "./admin-services";
const postsDriver = new DriverBuilder().withBaseURL("https://api.example.com").withServices(postsServices).build();
const adminDriver = new DriverBuilder().withBaseURL("https://admin.example.com").withServices(adminServices).build();SWR example (Axios)
import useSWR from "swr";
export function usePosts() {
const fetcher = () => httpApi.execService({ id: "posts.list" });
return useSWR("posts", fetcher);
}Examples
The repository includes runnable examples:
- Basic JSONPlaceholder and DummyJSON flows
- Version Configuration examples - Various versioning strategies and enable/disable patterns
- Version Builder examples - Comprehensive examples showing optional version building
- Advanced samples (AbortController, transforms, httpClientFetch)
- Full Builder demo (all hooks)
Run:
npm run start:exampleEntry point: example/index.ts
API reference (selected)
- DriverBuilder
- withBaseURL(baseURL: string)
- withServices(services: ServiceApi[])
- enableVersioning(enabled?: boolean) - Enable/disable version building
- withVersionTemplate(template: string) - Set custom template and auto-enable versioning
- withGlobalVersion(version: string | number)
- withVersionConfig(config: VersionConfig)
- withAddRequestTransformAxios(fn)
- withAddResponseTransformAxios(fn)
- withAddAsyncRequestTransformAxios(registrar)
- withAddAsyncResponseTransformAxios(registrar)
- withHandleInterceptorErrorAxios(fn)
- withAddRequestTransformFetch(fn)
- withAddTransformResponseFetch(fn)
- Driver instance
- execService({ id, params? }, payload?, options?)
- execServiceByFetch({ id, params? }, payload?, options?)
- getInfoURL({ id, params? }, payload?)
For implementation details, see source comments.
License
MIT
Author
Châu Lâm Đình Ái (alvin0)
GitHub: https://github.com/alvin0
Email: [email protected]
