npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 _state in 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-builder

Quick 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/1

Extending 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/activate

Using 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/1

TypeScript

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

  1. Global state + per-request state merged
  2. onBeforeRequest runs first
  3. Request interceptors (in order added) run next
  4. Request is sent
  5. On success: response interceptors → onAfterResponse
  6. On error: error interceptors → onError

License

MIT