chowbea-axios
Advanced

Authentication

Tokens, cookies, and the interceptors you’ll actually edit.

Auth is where generated clients either save you a week or quietly attach Bearer undefined forever. chowbea-axios generates an interceptor skeleton; you decide what “logged in” means.

api.instance.ts is generated once. After that it’s your file. Re-running fetch will not lovingly overwrite your refresh queue — though it may warn if [instance] config drifted.

auth_mode

ModeWhat you get
bearer-localstorageReads localStorage[token_key], attaches Authorization: Bearer …
custom (default)Pass-through interceptor with a TODO — implement your store
noneNo auth interceptor. Refreshing purity.
api.config.toml
[instance]
auth_mode = "bearer-localstorage"
token_key = "auth-token"
chowbea-axios init --auth-mode bearer-localstorage --token-key auth-token

What bearer-localstorage actually parses

api.instance.ts (generated shape)
axiosInstance.interceptors.request.use((config) => {
  if (typeof window === "undefined") return config;
  const raw = localStorage.getItem("auth-token");
  if (!raw) return config;

  try {
    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;
});

Handles plain strings, { token }, and Zustand-persist { state: { token } }. If your store is weirder, use custom.

The custom pattern you’ll ship

This is the production shape: read from a store, attach the header, refresh on 401 without stampedes.

api.instance.ts (sketch)
import axios from "axios";
import { axiosInstance } from "./api.instance";
// your app:
// import { useAuthStore } from "@/stores/auth";

axiosInstance.interceptors.request.use((config) => {
  const token = useAuthStore.getState().accessToken;
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

let refreshing = false;
let queue: Array<(token: string | null) => void> = [];

function flushQueue(token: string | null) {
  queue.forEach((cb) => cb(token));
  queue = [];
}

axiosInstance.interceptors.response.use(
  (res) => res,
  async (error) => {
    const original = error.config;
    const status = error.response?.status;
    const url = String(original?.url ?? "");

    // Don't refresh the refresh (or login) endpoint
    if (status !== 401 || original._retry || /\/auth\/(login|refresh)/.test(url)) {
      return Promise.reject(error);
    }

    if (refreshing) {
      return new Promise((resolve, reject) => {
        queue.push((token) => {
          if (!token) return reject(error);
          original.headers.Authorization = `Bearer ${token}`;
          resolve(axiosInstance(original));
        });
      });
    }

    original._retry = true;
    refreshing = true;

    try {
      const refreshToken = useAuthStore.getState().refreshToken;
      const { data } = await axios.post("/auth/refresh", { refreshToken });
      useAuthStore.getState().setTokens(data);
      flushQueue(data.accessToken);
      original.headers.Authorization = `Bearer ${data.accessToken}`;
      return axiosInstance(original);
    } catch (refreshError) {
      flushQueue(null);
      useAuthStore.getState().clear();
      window.location.href = "/login";
      return Promise.reject(refreshError);
    } finally {
      refreshing = false;
    }
  },
);

Wire the store however you like (Zustand persist, context, cookie jar). The point is: one place owns tokens, and concurrent 401s wait in line instead of starting a refresh festival.

Cookies

api.config.toml
[instance]
with_credentials = true

Default is false. Flip it when your API lives on another origin and authenticates with cookies — and make sure CORS agrees, or enjoy an afternoon of network-tab archaeology.

API keys

axiosInstance.interceptors.request.use((config) => {
  config.headers["X-API-Key"] = import.meta.env.VITE_API_KEY;
  return config;
});

Fetching a protected OpenAPI spec

Runtime auth ≠ spec-fetch auth. For a locked swagger URL:

api.config.toml
[fetch.auth]
type = "basic"
username = "$SWAGGER_USER"
password = "$SWAGGER_PASS"

# Optional extra headers for the spec download only
[fetch.headers]
"X-Internal-Token" = "$SPEC_TOKEN"
export SWAGGER_USER="ci-bot"
export SWAGGER_PASS="..."
chowbea-axios fetch

init can scaffold .github/workflows/chowbea-axios-ci.yml with secrets wiring. Pass --skip-workflow if CI is already your religion.

Handling UNAUTHORIZED in app code

const { error } = await api.op.getMe();
if (error?.code === "UNAUTHORIZED") {
  // interceptor may already be refreshing; this is the UI fallback
}

Prefer fixing 401s in the interceptor. Components should not each invent logout logic.

Next Steps

On this page