chowbea-axios
Advanced

Generated Files

Understanding the generated file structure and customization options.

When you run chowbea-axios init followed by fetch, the CLI generates a complete API client structure in your project. Understanding this structure is key to customizing behavior and extending functionality.

Why This Structure?

The file organization follows a clear separation of concerns:

  • Internal files (_internal/) — Caching and raw spec storage. Never edit these.
  • Generated files (_generated/) — TypeScript types and operations extracted from your spec. Overwritten on each generation.
  • Editable files (root level) — Your customization points. Created once, never overwritten.

This design means you can:

  1. Customize freely — Add interceptors, modify error handling, extend the client
  2. Stay in sync — Regenerate types without losing your changes
  3. Debug easily — Inspect the cached spec and cache metadata

File Structure Overview

.api-cache.json
openapi.json
api.types.ts
api.operations.ts
api.contracts.ts
api.client.ts
api.instance.ts
api.error.ts
api.helpers.ts

How Files Relate

┌──────────────────────────────────────────────────────────────────┐
│                        Your Application                          │
└──────────────────────────────────────────────────────────────────┘


                    ┌───────────────────────┐
                    │     api.client.ts     │  ◀── You import this
                    │   (main entry point)  │
                    └───────────────────────┘
                          │           │
            ┌─────────────┘           └─────────────┐
            ▼                                       ▼
┌───────────────────────┐               ┌───────────────────────┐
│   api.instance.ts     │               │  _generated/          │
│   (axios config)      │               │  api.operations.ts    │
└───────────────────────┘               │  api.types.ts         │
            │                           └───────────────────────┘
            ▼                                       │
┌───────────────────────┐                          │
│    api.error.ts       │  ◀───────────────────────┘
│    api.helpers.ts     │
└───────────────────────┘

File Categories

Auto-Managed Files (Don't Edit)

These files are overwritten on every fetch or generate:

FilePurpose
_internal/.api-cache.jsonCache metadata (hash, timestamp)
_internal/openapi.jsonCached OpenAPI spec (always JSON, regardless of source format)
_generated/api.types.tsTypeScript paths/components/operations from the spec
_generated/api.operations.tsTyped api.op.<operationId>() methods
_generated/api.contracts.tsConcrete named interfaces for each operation — these are what enable cmd+click navigation in editors

Never edit files in _internal/ or _generated/. Your changes will be lost on the next generation.

Editable Files (Generated Once)

These files are only created if they don't exist:

FilePurpose
api.client.tsMain typed API client
api.instance.tsAxios instance with interceptors
api.error.tsError types and Result handling
api.helpers.tsType utility helpers

You can safely modify these files—they won't be overwritten.

Detailed File Descriptions

api.types.ts

Generated by openapi-typescript, contains:

  • paths - All API path definitions
  • components - Schema definitions
  • operations - Operation type definitions
_generated/api.types.ts
export interface paths {
  "/users": {
    get: {
      parameters: { query?: { limit?: number } };
      responses: { 200: { content: { "application/json": User[] } } };
    };
    post: {
      requestBody: { content: { "application/json": CreateUserInput } };
      responses: { 201: { content: { "application/json": User } } };
    };
  };
  // ...
}

export interface components {
  schemas: {
    User: { id: string; name: string; email: string };
    CreateUserInput: { name: string; email: string };
    // ...
  };
}

api.operations.ts

Contains operation functions for endpoints with operationId:

_generated/api.operations.ts
export const createOperations = (apiClient: any) => ({
  /**
   * List all users
   * @operationId listUsers
   * @method GET
   * @path /users
   */
  listUsers: (config?: RequestConfig<"/users", "get">) =>
    apiClient.get("/users", config),

  /**
   * Get user by ID
   * @operationId getUserById
   * @method GET
   * @path /users/{id}
   */
  getUserById: (
    pathParams: { id: string | number },
    config?: RequestConfig<"/users/{id}", "get">
  ) => apiClient.get("/users/{id}", pathParams, config),
  
  // ...
});

api.client.ts

The main API client that you import in your application:

api.client.ts
import { axiosInstance } from "./api.instance";
import { safeRequest } from "./api.error";
import { createOperations } from "./_generated/api.operations";

const api = {
  get<P extends Paths>(...) { /* ... */ },
  post<P extends Paths>(...) { /* ... */ },
  put<P extends Paths>(...) { /* ... */ },
  delete<P extends Paths>(...) { /* ... */ },
  patch<P extends Paths>(...) { /* ... */ },
  
  // Operation-based API
  get op() {
    return createOperations(this);
  },
};

export { api };

api.instance.ts

Axios instance with an auth interceptor whose shape is driven by the auth_mode setting in your config. The three variants:

auth_mode = "bearer-localstorage" (SPA pattern — reads a token from localStorage):

api.instance.ts (auth_mode = bearer-localstorage)
import axios from "axios";

export const tokenKey = "auth-token";

export const axiosInstance = axios.create({
  baseURL: process.env.API_BASE_URL,
  withCredentials: true,
  timeout: 30000,
});

axiosInstance.interceptors.request.use((config) => {
  if (typeof window !== "undefined") {
    const raw = localStorage.getItem(tokenKey);
    if (raw) {
      try {
        // Handles plain strings, { token }, and Zustand-style { state: { token } }
        const parsed = JSON.parse(raw);
        const token = parsed.state?.token ?? parsed.token ?? parsed;
        if (typeof token === "string") {
          config.headers.Authorization = `Bearer ${token}`;
        }
      } catch {
        config.headers.Authorization = `Bearer ${raw}`;
      }
    }
  }
  return config;
});

auth_mode = "custom" (default — emits a TODO placeholder for you to fill in):

api.instance.ts (auth_mode = custom)
import axios from "axios";

export const axiosInstance = axios.create({
  baseURL: process.env.API_BASE_URL,
  withCredentials: true,
  timeout: 30000,
});

axiosInstance.interceptors.request.use((config) => {
  // TODO: implement your auth — e.g. read from a store, cookie, or session
  return config;
});

auth_mode = "none" — no interceptor is attached. Useful for public APIs or when auth is handled at a higher layer.

The env_accessor config controls whether the baseURL line is process.env.X or import.meta.env.X. See authentication for the full breakdown.

api.error.ts

Error handling utilities:

api.error.ts
export interface ApiError {
  message: string;
  code: string;
  status: number | null;
  request: RequestContext;
  details?: unknown;
}

export type Result<T> =
  | { data: T; error: null }
  | { data: null; error: ApiError };

export async function safeRequest<T>(
  promise: Promise<AxiosResponse<T>>
): Promise<Result<T>> {
  try {
    const response = await promise;
    return { data: response.data, error: null };
  } catch (err) {
    return { data: null, error: createApiError(err) };
  }
}

api.helpers.ts

Type utilities for extracting types from the spec:

api.helpers.ts
export type ApiRequestBody<P extends Paths, M extends HttpMethod> = ...;
export type ApiResponseData<P extends Paths, M extends HttpMethod> = ...;
export type ServerModel<ModelName extends keyof components["schemas"]> = ...;
// ...

Customizing Editable Files

Custom Interceptors

Add response interceptors in api.instance.ts:

api.instance.ts
// Add response interceptor for logging
axiosInstance.interceptors.response.use(
  (response) => {
    console.log(`[API] ${response.config.method} ${response.config.url}`, response.status);
    return response;
  },
  (error) => {
    console.error(`[API Error]`, error.response?.status, error.message);
    return Promise.reject(error);
  }
);

Custom Token Handling

Modify the token retrieval logic:

api.instance.ts
axiosInstance.interceptors.request.use((config) => {
  // Custom: Read from a different source
  const session = sessionStorage.getItem("session");
  if (session) {
    const { accessToken } = JSON.parse(session);
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

Custom Error Normalization

Extend error handling in api.error.ts:

api.error.ts
export function normalizeErrorMessage(error: unknown): string {
  // Add custom format handling
  if (error && typeof error === "object") {
    const e = error as Record<string, unknown>;
    
    // Your API's custom format
    if (e.errorMessage && typeof e.errorMessage === "string") {
      return e.errorMessage;
    }
  }
  
  // Fall back to default handling
  // ... existing code ...
}

Adding Custom Methods

Extend the API client in api.client.ts:

api.client.ts
const api = {
  // ... existing methods ...
  
  // Custom: Batch requests
  async batch<T>(requests: Promise<Result<T>>[]): Promise<Result<T>[]> {
    return Promise.all(requests);
  },
  
  // Custom: Health check
  async healthCheck() {
    return this.get("/health");
  },
};

Regenerating Editable Files

If you want to regenerate the editable files (reset to defaults):

chowbea-axios init --force

This will overwrite your customizations. Back up any changes first!

Next Steps

On this page