Introduction
Do you experience this every time you write HTTP requests?
// Configuration is too complex...
const response = await axios.post('https://api.example.com/users', data, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 5000,
httpsAgent: new https.Agent({
rejectUnauthorized: false // Writing this for self-signed certificates every time...
})
});
const user = response.data; // Why do I need to write .data every single time?
// Error handling is a nightmare
try {
await axios.get('/api/data');
} catch (error) {
// error.response?.data?.message? error.response?.status? error.config?.url?
// Always checking docs to find where things are...
console.log('Status:', error.response?.status);
console.log('Message:', error.response?.data?.message);
console.log('URL:', error.config?.url);
}
To solve these developer frustrations, I created axios-fluent. This is my first published npm package, built from real pain points I experienced in production projectsβevery "annoying thing about Axios" packed into one practical library.
npm install axios-fluent
https://www.npmjs.com/package/axios-fluent
π― 3 Problems axios-fluent Solves
1. Complex Request Building β Clean with Builder Style
Before (Plain Axios)
const config = {
baseURL: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 10000
};
const instance = axios.create(config);
const response = await instance.get('/users/1');
const user = response.data;
After (axios-fluent)
const client = Axon.new()
.baseUrl('https://api.example.com')
.bearer(token)
.json()
.timeout(10000);
const user = await client.get('/users/1').data();
π‘ Technical Deep Dive: Immutable Builder Pattern
Each method in axios-fluent returns a new instance by design.
public bearer(token: string): Axon {
const newConfig = {
...this.config,
headers: { ...this.config.headers, Authorization: `Bearer ${token}` },
};
return new Axon(newConfig, this.instance); // Returns new instance
}
This enables:
- Natural method chaining
- No mutation of original instance (no side effects)
- Follows functional programming principles
2. Axios Errors Too Verbose β AxonError with Only Essential Info
Before (Plain Axios)
try {
await axios.get('/api/users');
} catch (error) {
// Information scattered across different properties...
const status = error.response?.status;
const message = error.response?.data?.message;
const url = error.config?.url;
const method = error.config?.method;
console.log(`${method} ${url} failed with ${status}: ${message}`);
// Writing this every time is exhausting
}
After (axios-fluent)
try {
await client.get('/api/users');
} catch (error) {
if (error instanceof AxonError) {
console.log(error.toString());
// β Automatically formatted output
// AxonError: Request failed with status code 404
// Request: GET https://api.example.com/api/users
// Status: 404 Not Found
// Response: {"error":"User not found"}
}
}
π‘ Technical Deep Dive: Only 5 Essential Properties
export class AxonError extends Error {
public readonly status?: number; // HTTP status code
public readonly statusText?: string; // Status text
public readonly url?: string; // Request URL
public readonly method?: string; // HTTP method
public readonly responseData?: any; // Response body
public toString(): string {
const parts = [`AxonError: ${this.message}`];
if (this.method && this.url) {
parts.push(` Request: ${this.method} ${this.url}`);
}
if (this.status) {
parts.push(` Status: ${this.status} ${this.statusText || ''}`);
}
if (this.responseData) {
parts.push(` Response: ${JSON.stringify(this.responseData)}`);
}
return parts.join('\n');
}
}
In v2.0.0, I removed unnecessary properties (headers, originalError) and focused on only the information you actually need for debugging.
3. Self-Signed Certificate Error Nightmare β allowInsecure Has Your Back
When using self-signed HTTPS certificates in development, you end up writing this every time:
Before (Plain Axios)
const https = require('https');
const instance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false // Copy-pasting this for every dev environment...
})
});
After (axios-fluent)
// Convenient factory for development
const client = Axon.dev();
// Or explicitly specify
const client = Axon.new({ allowInsecure: true });
π‘ Technical Deep Dive: Secure by Default, Easy for Development
public static new(options: Options = { allowInsecure: false }) {
const config: AxiosRequestConfig = {};
if (options?.allowInsecure) {
config.httpsAgent = new https.Agent({
rejectUnauthorized: false, // Only disabled when explicitly opted-in
});
}
return new Axon(config);
}
public static dev() {
return Axon.new({ allowInsecure: true }); // Development shortcut
}
Security-First Design:
- Default is certificate validation enabled (safe for production)
-
allowInsecurerequires explicit opt-in -
Axon.dev()is a convenience tool for development (purpose clear from name)
π¦ Practical Usage
Building an API Client Wrapper
import { Axon } from 'axios-fluent';
class ApiClient {
private client: Axon;
constructor(baseUrl: string, token?: string) {
this.client = Axon.new()
.baseUrl(baseUrl)
.json()
.timeout(10000);
if (token) {
this.client = this.client.bearer(token);
}
}
async getUser(id: number): Promise<User> {
return await this.client.get<User>(`/users/${id}`).data();
}
async createUser(data: CreateUserDto): Promise<User> {
return await this.client.post<User>('/users', data).data();
}
async uploadAvatar(userId: number, file: File): Promise<void> {
const formData = new FormData();
formData.append('avatar', file);
await this.client
.multipart()
.post(`/users/${userId}/avatar`, formData);
}
}
export const api = new ApiClient(
process.env.API_URL || 'https://api.example.com',
localStorage.getItem('authToken') || undefined
);
Response Convenience Methods Reduce Boilerplate
// Before: Writing .data every time
const response = await axios.get<User>('/users/1');
const user = response.data;
const status = response.status;
// After: Get exactly what you want directly
const user = await client.get<User>('/users/1').data();
const status = await client.get('/users/1').status();
const isOk = await client.post('/users', data).ok(); // Check if 2xx
π‘ Technical Deep Dive: PromiseLike for Backward Compatibility
export class AxonResponse<T = any> implements PromiseLike<AxiosResponse<T>> {
async data(): Promise<T> {
const response = await this.responsePromise;
return response.data;
}
async status(): Promise<number> {
const response = await this.responsePromise;
return response.status;
}
async ok(): Promise<boolean> {
const response = await this.responsePromise;
return response.status >= 200 && response.status < 300;
}
// Promise interface for backward compatibility
then<TResult1 = AxiosResponse<T>, TResult2 = never>(
onfulfilled?: ((value: AxiosResponse<T>) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): PromiseLike<TResult1 | TResult2> {
return this.responsePromise.then(onfulfilled, onrejected);
}
}
This design adds new features without breaking existing code.
Error Handling Best Practices
import { AxonError } from 'axios-fluent';
async function fetchUserWithRetry(id: number, maxRetries = 3): Promise<User> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await client.get<User>(`/users/${id}`).data();
} catch (error) {
if (error instanceof AxonError) {
// Branch logic based on status code
if (error.status === 404) {
throw new Error(`User ${id} not found`);
}
if (error.status && error.status >= 500) {
// Retry server errors
if (attempt === maxRetries) {
throw new Error(`Server error after ${maxRetries} attempts: ${error.toString()}`);
}
await sleep(1000 * attempt); // Exponential backoff
continue;
}
// Don't retry 4xx errors
throw error;
}
// Non-AxonError (network errors, etc.)
throw error;
}
}
throw new Error('Unexpected error in retry logic');
}
Implementing Authentication Flow
class AuthService {
private client: Axon;
private refreshToken?: string;
constructor(baseUrl: string) {
this.client = Axon.new().baseUrl(baseUrl).json();
}
async login(email: string, password: string): Promise<void> {
const response = await this.client
.post<AuthResponse>('/auth/login', { email, password })
.data();
this.refreshToken = response.refreshToken;
this.client = this.client.bearer(response.accessToken);
}
async refreshAccessToken(): Promise<void> {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = await this.client
.post<AuthResponse>('/auth/refresh', { refreshToken: this.refreshToken })
.data();
this.refreshToken = response.refreshToken;
this.client = this.client.bearer(response.accessToken);
}
async fetchProtectedResource<T>(url: string): Promise<T> {
try {
return await this.client.get<T>(url).data();
} catch (error) {
if (error instanceof AxonError && error.status === 401) {
// Token expired β Refresh and retry
await this.refreshAccessToken();
return await this.client.get<T>(url).data();
}
throw error;
}
}
}
π Why You Need axios-fluent
1. Faster Development
- Builder pattern makes request configuration intuitive
- Response convenience methods reduce boilerplate code
- Simplified error handling
2. Improved Code Readability
// Before: Deep nesting
const response = await axios.post(
'https://api.example.com/users',
data,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 5000
}
);
// After: Flat and readable
const user = await Axon.new()
.baseUrl('https://api.example.com')
.bearer(token)
.json()
.timeout(5000)
.post('/users', data)
.data();
3. Type Safety Maintained
interface User {
id: number;
name: string;
email: string;
}
// Full TypeScript type inference
const user: User = await client.get<User>('/users/1').data();
// ^? Inferred as User type
4. Production-Ready
- Secure by default (HTTPS certificate validation ON)
- Development convenience maintained (
Axon.dev()) - Systematic error handling
- 100% backward compatibility (doesn't break existing code)
π Performance & Bundle Size
axios-fluent is an Axios wrapper, so performance overhead is nearly zero.
| Metric | Value |
|---|---|
| Package Size | ~17KB (minified) |
| Dependencies | axios (peer), form-data |
| TypeScript Support | β Full support |
| Node.js Support | v20+ |
Summary
axios-fluent is my first published npm package, designed to solve pain points I felt in real development.
β 3 Major Improvements
- Builder pattern makes request construction easy
- AxonError makes error handling intuitive
- allowInsecure frees you from development certificate errors
β Practical-Focused Design
- Secure by default, convenient for development
- 100% backward compatibility (doesn't break existing code)
- Maintains TypeScript type safety
- Provides 6 practical code examples
If you've ever thought "Axios configuration is complex," "error handling is tedious," or "self-signed certificates always error out," give it a try.
npm install axios-fluent
π¦ NPM: https://www.npmjs.com/package/axios-fluent
π GitHub: https://github.com/oharu121/axios-fluent
Feedback and contributions are welcome!
Top comments (0)