@e12e/axios-builder
v1.0.0
Published
Fluent builder for creating typed Axios instances with state management, lifecycle hooks, and interceptors.
Readme
@e12e/axios-builder
Fluent builder for creating typed Axios instances with state management, lifecycle hooks, and interceptors.
Features
- Builder pattern — chainable API for configuring clients
- Typed global state — attach mutable state to client instances
- Per-request state — override state per request via
_statein config - Lifecycle hooks —
onBeforeRequest,onAfterResponse,onError - Interceptors — request/response interceptors with merged state access
- HTTP convenience —
get,post,put,patch,delete,head,options - Controller builder — scoped HTTP client with path prefix
- Lazy build — instance created on first HTTP call, no
.build()required - TypeScript — full generic type support
Installation
pnpm add @e12e/axios-builderQuick Start
import { AxiosBuilder } from "@e12e/axios-builder";
interface MyState {
token: string;
}
const apiClient = new AxiosBuilder<MyState>()
.setConfig({ baseURL: "https://api.example.com" })
.setState({ token: "initial-token" })
.onBeforeRequest((config, state) => {
config.headers.Authorization = `Bearer ${state.token}`;
return config;
})
.onError((err, state) => {
console.error("Request failed:", err.message);
throw err;
});
// Call directly — no .build() needed
const response = await apiClient.get("/users");
// Update global state at runtime
apiClient.setState({ token: "new-token" });Per-request State
Override global state per request via _state in config. Interceptors receive the merged state (global + per-request, per-request takes precedence).
interface AppState {
token: string;
retry: boolean;
}
const apiClient = new AxiosBuilder<AppState>()
.setConfig({ baseURL: "/api" })
.setState({ token: "abc", retry: false })
.addRequestInterceptor((config, state) => {
config.headers.Authorization = `Bearer ${state.token}`;
return config;
})
.onError((err, state) => {
if (state.retry && err.response?.status === 503) {
return apiClient.get(err.config?.url || "");
}
throw err;
});
// Normal request — retry is false
await apiClient.get("/users");
// This request will auto-retry on 503
await apiClient.get("/critical", { _state: { retry: true } });HTTP Convenience Methods
All methods delegate to the underlying AxiosInstance. No .build() required.
const apiClient = new AxiosBuilder()
.setConfig({ baseURL: "https://api.example.com" });
// GET
const res = await apiClient.get<User[]>("/users");
// POST
const res = await apiClient.post<User>("/users", { name: "Em" });
// PUT
const res = await apiClient.put<User>("/users/1", { name: "Updated" });
// PATCH
const res = await apiClient.patch<User>("/users/1", { name: "Patched" });
// DELETE
await apiClient.delete("/users/1");
// HEAD / OPTIONS
await apiClient.head("/users");
await apiClient.options("/users");
// With per-request config
const res = await apiClient.get("/users", { timeout: 5000, _state: { retry: true } });Controller Builder
Create scoped HTTP clients with a path prefix using controller(). All requests will have the path prepended automatically.
const apiClient = new AxiosBuilder()
.setConfig({ baseURL: "https://api.example.com" });
// Create a controller for /users endpoint
const userApi = apiClient.controller("users");
// GET /users
await userApi.get();
// POST /users
await userApi.post({ name: "Em" });
// GET /users/1 (relative url - path is prepended)
await userApi.get("1");
// PUT /users/1
await userApi.put("1", { name: "Updated" });
// DELETE /users/1
await userApi.delete("1");
// GET /other (absolute url starting with / - path is NOT prepended)
await userApi.get("/other");URL Behavior
- Relative URL (e.g.
"1") → Prepends path:users/1 - Absolute URL (e.g.
"/other") → Uses URL as-is:/other
Nested Controllers
const api = apiClient.controller("api");
const userApi = api.controller("users");
// GET /api/users
await userApi.get();
// GET /api/users/1
await userApi.get("1");Controller with State
Controllers share state, hooks, and interceptors with the parent builder:
const apiClient = new AxiosBuilder<{ token: string }>()
.setState({ token: "abc" })
.onBeforeRequest((config, state) => {
config.headers.Authorization = `Bearer ${state.token}`;
return config;
});
const userApi = apiClient.controller("users");
// Request will include Authorization header
await userApi.get();Entity CRUD Template
Create reusable CRUD templates for your entities by extending ControllerBuilder:
import { ControllerBuilder } from "@e12e/axios-builder";
// Base CRUD template
class BaseEntityApi<T> extends ControllerBuilder {
/** GET /entity */
getAll() {
return this.get<T[]>();
}
/** GET /entity/:id */
getById(id: string) {
return this.get<T>(id);
}
/** POST /entity */
create(data: Partial<T>) {
return this.post<T>("", data);
}
/** PUT /entity/:id */
update(id: string, data: Partial<T>) {
return this.put<T>(id, data);
}
/** DELETE /entity/:id */
remove(id: string) {
return this.delete(id);
}
}
// Usage
interface User {
id: string;
name: string;
email: string;
}
const userApi = new BaseEntityApi<User>("users", apiClient);
await userApi.getAll(); // GET /users
await userApi.getById("1"); // GET /users/1
await userApi.create({ name: "Em" });// POST /users
await userApi.update("1", { name: "Updated" }); // PUT /users/1
await userApi.remove("1"); // DELETE /users/1Extending with Custom Methods
class UserApi extends BaseEntityApi<User> {
/** GET /users/by-email/:email */
getByEmail(email: string) {
return this.get<User>(`/by-email/${email}`);
}
/** POST /users/:id/activate */
activate(id: string) {
return this.post<User>(`${id}/activate`);
}
}
const userApi = new UserApi("users", apiClient);
await userApi.getByEmail("[email protected]"); // GET /users/by-email/[email protected]
await userApi.activate("1"); // POST /users/1/activateUsing with Nested Controllers
const api = apiClient.controller("api/v1");
const userApi = new BaseEntityApi<User>("users", api);
// GET /api/v1/users
await userApi.getAll();
// GET /api/v1/users/1
await userApi.getById("1");API
new AxiosBuilder<TState>()
Creates a new builder instance with optional typed state.
const builder = new AxiosBuilder<{ token: string; userId: string }>();.setConfig(config)
Merge Axios request config (baseURL, timeout, headers, etc.).
.setState(state)
Set global state (shallow merge). Available in all interceptors.
.onBeforeRequest(fn)
Set a hook that runs before every request. Receives config and merged state.
.onAfterResponse(fn)
Set a hook that runs after every successful response. Receives response and merged state.
.onError(fn)
Set a hook that runs on every error. Receives AxiosError and merged state.
.addRequestInterceptor(fn)
Add a request interceptor. Receives config and merged state.
.addResponseInterceptor(onFulfilled, onRejected?)
Add a response interceptor. Both functions receive state.
.build()
Build and return a NEW AxiosInstance. Use when you need multiple independent instances.
.getInstance()
Get the lazily-created instance. Creates one if it doesn't exist yet.
.controller(path)
Create a scoped HTTP client with a path prefix. Returns a ControllerBuilder instance.
const userApi = apiClient.controller("users");
await userApi.get(); // GET /users
await userApi.get("1"); // GET /users/1TypeScript
Full generic support for state type:
interface AuthState {
token: string;
refreshToken: string;
isRefreshing: boolean;
}
const apiClient = new AxiosBuilder<AuthState>()
.setState({ token: "", refreshToken: "", isRefreshing: false })
.addRequestInterceptor((config, state) => {
config.headers.Authorization = `Bearer ${state.token}`;
return config;
});
const res = await apiClient.get("/profile");Execution Order
- Global state + per-request state merged
onBeforeRequestruns first- Request interceptors (in order added) run next
- Request is sent
- On success: response interceptors →
onAfterResponse - On error: error interceptors →
onError
License
MIT
