DEV Community

Yu-Chen, Lin
Yu-Chen, Lin

Posted on

Stop Struggling with Axios! My First NPM Package "axios-fluent" Solves 3 Major Pain Points

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?
Enter fullscreen mode Exit fullscreen mode
// 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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"}
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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...
  })
});
Enter fullscreen mode Exit fullscreen mode

After (axios-fluent)

// Convenient factory for development
const client = Axon.dev();

// Or explicitly specify
const client = Axon.new({ allowInsecure: true });
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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
}
Enter fullscreen mode Exit fullscreen mode

Security-First Design:

  • Default is certificate validation enabled (safe for production)
  • allowInsecure requires 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
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ 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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Builder pattern makes request construction easy
  2. AxonError makes error handling intuitive
  3. 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
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ NPM: https://www.npmjs.com/package/axios-fluent
πŸ“‚ GitHub: https://github.com/oharu121/axios-fluent

Feedback and contributions are welcome!

axios #npm

Top comments (0)