Getting Started
Set up chowbea-axios in your project in under 5 minutes.
You have an OpenAPI spec and a frontend that deserves better than any. This guide walks the full path: spec → generate → call → unwrap → query → ship. The first three steps happen here; the rest lives in The Query Layer.
Under five minutes. Longer if your backend isn't running yet — we can't fix that.
Step 1: Initialize Your Project
Run init in your project root:
chowbea-axios initYou'll be prompted for:
- OpenAPI spec endpoint URL — where your API docs are served
- Output folder — where generated files go (default:
app/services/api) - Package manager — detected automatically, overridable
Init installs axios, adds api:* scripts to package.json, and scaffolds client files. One command, not a checklist.
What Gets Created
Generated output appears after your first fetch.
NPM Scripts Added
{
"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 with your dev server:
{
"scripts": {
"dev:all": "concurrently \"npm run api:watch\" \"npm run dev\""
}
}dev:all = types regenerate while you work. Run it. Future you will not send thank-you notes, but you'll notice fewer "wait, did the API change?" moments.
Step 2: Fetch Your OpenAPI Spec
Start your API server, then:
npm run api:fetchThis command:
- Downloads your OpenAPI spec from the configured endpoint
- Caches it locally for change detection
- Generates TypeScript types, operations, and contracts
Generated File Structure
Files in _internal/ and _generated/ are overwritten on each fetch. Don't edit them. Customizations go in api.client.ts, api.instance.ts, and your app code.
Import types from api.contracts.ts — UserDto, CreateUserBody, and friends. Named interfaces, cmd+click navigation, request bodies distinct from entity DTOs. Helpers like ServerModel<"…"> exist for edge cases; contracts are the default.
Step 3: Call the API
Import the client and make a typed call:
import { api } from "./app/services/api/api.client";
import type { UserDto } from "./app/services/api/_generated/api.contracts";
// Operation-based (preferred)
const { data, error } = await api.op.getUserById({ id: "123" });
if (error) {
console.error(error.message);
console.error(error.code); // e.g., "NOT_FOUND", "UNAUTHORIZED"
console.error(error.status); // HTTP status code
return;
}
// data matches UserDto from your spec
console.log(data.name);
console.log(data.email);Path-based calls work too:
const { data, error } = await api.get("/users/{id}", { id: "123" });POST Request with Body
import type { CreateUserBody } from "./app/services/api/_generated/api.contracts";
const body: CreateUserBody = {
name: "John Doe",
email: "john@example.com",
};
const { data, error } = await api.op.createUser(body);
if (error) {
console.error(error.message);
return;
}
console.log("Created user:", data.id);Every call returns { data, error } — no thrown exceptions at the transport layer. Check error before using data.
Step 4: Unwrap and Query (Where Apps Actually Live)
Raw { data, error } in every component is how tech debt starts. The recommended stack:
api.contracts.ts → api.op → exec → TanStack- Types from
api.contracts.ts - Calls via
api.op.* - Unwrap once in a query class (
execthrowsQueryErroron failure) - Expose
queryOptions/ hooks to components
// Sketch — full pattern in the query layer docs
protected async exec<T>(promise: Promise<Result<T>>): Promise<T> {
const { data, error } = await promise;
if (error) throw new QueryError(error);
return data;
}Components import hooks, not api.op directly. One cache, one error shape, one place to fix things when the spec changes.
→ The Query Layer — the path that ships.
Step 5: Watch Mode (Development)
During development, keep types in sync automatically:
npm run api:watchPolls your API endpoint and regenerates when the spec changes. Combine with your dev server via dev:all (see Step 1).
Change a backend DTO, save, watch regenerates, TypeScript shows what broke. Fix it before merge. That's the whole philosophy in one workflow.