chowbea-axios

Introduction

Generate type-safe Axios clients from OpenAPI specs — typed Axios, autocomplete, and Result-based errors for TypeScript.

You're here because you've typed response.data as User one too many times, or because your "temporary" hand-written API types are now a second job. Fair.

chowbea-axios reads your OpenAPI spec and generates a typed Axios client — types, operations, contracts, and a { data, error } calling convention. One source of truth. Your IDE stops guessing.

The path that actually ships: spec → generate → call → unwrap → query → ship. This page covers the first half; The Query Layer covers the rest.

The Problem (You Already Know It)

Working with REST in TypeScript usually looks like this:

  • Manual types that drift the moment someone adds a field on the backend
  • No autocomplete for paths, params, or response shapes
  • try/catch archaeology with a different error shape per endpoint
  • Production surprises when the API changed Tuesday and nobody told the frontend

You end up maintaining two contracts: the OpenAPI spec and whatever you typed in types/api.ts. They will diverge. It's not a question of if.

The Solution

Point chowbea-axios at your spec. It generates:

  • Types extracted from the spec — always in sync
  • A typed client with autocomplete on every endpoint
  • Named contracts in api.contracts.ts — cmd+click to real interfaces, not string-keyed lookups
  • Normalized errors with a consistent shape across all calls
  • Watch mode that regenerates when the spec changes

One command. Zero manual DTO maintenance. Your IDE knows your entire API because it is your entire API.

How It Works

Understanding the pipeline helps you debug the one day generation "does nothing" (spoiler: hash cache, working as intended).

1. Configuration

Everything starts with api.config.toml, created by init:

api_endpoint = "http://localhost:3000/docs/swagger/json"
poll_interval_ms = 10000

[output]
folder = "src/services/api"

[instance]
base_url_env = "VITE_API_URL"
timeout = 30000

Where to fetch the spec, where to write files, how to configure Axios. Boring on purpose — that's the point.

2. Fetching and Caching

When you run fetch, the CLI:

  1. Reads api_endpoint from config (or spec_file for local specs)
  2. Downloads the OpenAPI JSON into _internal/openapi.json
  3. Hashes the spec and saves it to _internal/.api-cache.json

On subsequent runs, if the hash matches, generation is skipped. fetch feels instant after the first run because it's doing nothing — which is correct behavior, not a bug.

3. Type Generation

The generator parses your spec and extracts:

  • Paths/users/{id}api.get("/users/{id}", { id })
  • Schemascomponents.schemas → interfaces in api.types.ts
  • OperationsoperationIdapi.op.getUserById()
  • Contracts — named interfaces per operation in api.contracts.ts (UserDto, CreateUserBody, …)
  • Parameters — path, query, and body params, all typed

Three generated files land in _generated/:

  • api.types.ts — schema types
  • api.operations.ts — operation-based methods
  • api.contracts.tsstart here for day-to-day imports

The Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                           chowbea-axios fetch                           │
└─────────────────────────────────────────────────────────────────────────┘


                    ┌───────────────────────────────┐
                    │    Read api.config.toml       │
                    │    (endpoint, output folder)  │
                    └───────────────────────────────┘


                    ┌───────────────────────────────┐
                    │    Fetch OpenAPI Spec         │
                    │    (remote URL or local file) │
                    └───────────────────────────────┘


                    ┌───────────────────────────────┐
                    │    Compute Spec Hash          │
                    └───────────────────────────────┘


                         ┌──────────────────┐
                         │  Hash Changed?   │
                         └──────────────────┘
                           │              │
                      No   │              │  Yes
                           ▼              ▼
              ┌─────────────────┐   ┌─────────────────────┐
              │  Skip Generation │   │  Parse & Generate   │
              │  (use cached)    │   │                     │
              └─────────────────┘   └─────────────────────┘

                        ┌─────────────────────┴─────────────────────┐
                        ▼                                           ▼
          ┌─────────────────────────┐             ┌─────────────────────────┐
          │    api.types.ts         │             │   api.operations.ts     │
          │    (from schemas)       │             │   (from operationIds)   │
          └─────────────────────────┘             └─────────────────────────┘


                              ┌─────────────────────────┐
                              │   api.contracts.ts      │
                              │   (named interfaces)    │
                              └─────────────────────────┘

If the spec hasn't changed, generation is skipped. If it has, only _generated/ is overwritten — your edits in api.client.ts and api.instance.ts survive.

Two Ways to Call Your API

Both are fully typed. Pick what reads better; the recommended app stack prefers operation-based calls.

Operation-based — reference endpoints by operationId (preferred in app code):

const { data, error } = await api.op.getUserById({ id: "123" });

Path-based — reference endpoints by URL path:

const { data, error } = await api.get("/users/{id}", { id: "123" });

Path-based is intuitive if you think in REST resources. Operation-based is cleaner when your spec has well-named operationIds — and it's what The Query Layer builds on.

Result-Based Errors

API calls return { data, error } instead of throwing:

const { data, error } = await api.op.getUserById({ id: "123" });

if (error) {
  console.log(error.status);   // HTTP status code
  console.log(error.message);  // Normalized message
  return;
}

// data is fully typed here
console.log(data.name);

No try/catch roulette. Every error has the same structure — network failure, 404, validation error. In app code you'll unwrap once in a query class; components shouldn't negotiate with Result directly. See The Query Layer for that pattern.

Once you've generated the client, don't stop at api.op in random components:

api.contracts.ts  →  named types (UserDto, CreateUserBody, …)
api.op.*            →  typed calls returning Result<T>
BaseQuery.exec      →  unwrap to data or QueryError
*.options.ts        →  queryOptions / mutationOptions
use-*.tsx           →  hooks your components import

Full walkthrough: The Query Layer.

What Gets Generated

Your OpenAPI spec becomes:

api.types.ts
api.operations.ts
api.contracts.ts
api.client.ts
api.instance.ts
api.error.ts
api.helpers.ts

_generated/ is overwritten on each generation. Everything else is created once and safe to customize — interceptors, base URL, error handling.

Import types from api.contracts.ts, not ServerModel<"…">. The helpers exist; the contracts are the default.

Daily Scripts

init adds these to your package.json:

{
  "scripts": {
    "api:fetch": "chowbea-axios fetch",
    "api:generate": "chowbea-axios generate",
    "api:watch": "chowbea-axios watch",
    "api:status": "chowbea-axios status",
    "api:validate": "chowbea-axios validate",
    "api:diff": "chowbea-axios diff"
  }
}

Pair watch mode with your dev server so types stay warm:

{
  "scripts": {
    "dev:all": "concurrently \"npm run api:watch\" \"npm run dev\""
  }
}

dev:all = backend types regenerate while you break the UI. Ideal.

Built for Developer Experience

  • Self-healing — auto-creates directories and guides setup
  • Smart caching — skips regeneration when the spec hasn't changed
  • Retry logic — network requests retry with exponential backoff
  • Atomic writes — generation never leaves half-written files
  • Graceful shutdown — watch mode preserves cache on interruption
  • Interactive dashboard — run chowbea-axios with no command for an OpenTUI dashboard (fetch, generate, diff, validate, watch, plugins, endpoint inspector). Bun required for the TUI; headless CLI works under Node alone
  • CI-friendly--non-interactive mode plus a hardened workflow template

Support the Project

If this saves you from another as any, consider a star on GitHub. It helps others find it and funds the occasional sanity-preserving feature.

Next Steps

On this page