chowbea-axios
Usage

Using the API Client

Make type-safe Axios API calls with the generated OpenAPI client — get, post, put, patch, delete, and more.

init writes api.client.ts. Some teams rename it or re-export as api.http.ts. Fine — pick one import path and stop bikeshedding. The generator default is api.client.ts; the examples below use that.

The path client gives you fully typed HTTP against your OpenAPI spec. Every call returns { data, error }. It does not throw. Your query layer can throw; the client won't surprise you mid-await.

For day-to-day app code, prefer api.op and types from api.contracts.ts. Path-based calls are the escape hatch — exploring an unfamiliar spec, endpoints without operationId, or when you genuinely want the URL in the call site. See The Query Layer for the stack that actually scales.

Basic usage

Basic API calls
import { api } from "./app/services/api/api.client";

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

const { data, error } = await api.post("/users", {
  name: "John Doe",
  email: "john@example.com",
});

const { data, error } = await api.put("/users/{id}",
  { name: "Jane Doe" },
  { id: "123" }
);

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

const { data, error } = await api.patch("/users/{id}",
  { name: "Updated Name" },
  { id: "123" }
);

All eight HTTP methods are on the path client: get, post, put, patch, delete, head, options, and trace. Most apps live in the first five. The rest exist because your spec might, and pretending otherwise helps nobody.

Path parameters

Templates like /users/{id} take path params as the last argument before config:

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

const { data, error } = await api.get("/users/{userId}/posts/{postId}", {
  userId: "123",
  postId: "456",
});

Miss a required param and TypeScript complains. That's the point.

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

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

Query parameters

Pass them through config.params:

const { data, error } = await api.get("/users", {
  params: {
    limit: 10,
    offset: 0,
    sort: "createdAt",
  },
});

Types come from the spec. If your OpenAPI says limit is an integer, TypeScript knows.

const { data, error } = await api.get("/users", {
  params: {
    limit: 10,
    offset: 0,
    search: "john",
  },
});

Request bodies

POST, PUT, and PATCH bodies are inferred from the spec:

const { data, error } = await api.post("/users", {
  name: "John Doe",
  email: "john@example.com",
  bio: "Developer",
});

Skip a required field and the compiler catches it before your users do.

// TypeScript error: 'email' is required
const { data, error } = await api.post("/users", {
  name: "John Doe",
});

Prefer named body types from api.contracts.ts (CreateUserBody, etc.) when wiring services or query classes — see Type Helpers.

Response types

Success payloads are typed from your spec:

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

if (error) {
  return;
}

console.log(data.id);
console.log(data.name);
console.log(data.email);
console.log(data.unknown); // TypeScript error — good

Result-based error handling

The client never throws. You get a discriminated union:

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

Handle failures explicitly:

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

if (error) {
  console.error(error.message);
  console.error(error.code);   // "NOT_FOUND", "UNAUTHORIZED", etc.
  console.error(error.status);
  return;
}

console.log(data.name); // data is narrowed here

See Error Handling for unwrap patterns. In a TanStack stack, the query class throws once — components shouldn't juggle Result.

Additional Axios config

Axios options go in the last argument:

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

const { data, error } = await api.get("/users", {
  headers: {
    "X-Custom-Header": "value",
  },
});

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

Method signatures

GET, DELETE, HEAD, OPTIONS, TRACE

api.get<Path>(url, config?)
api.get<Path>(url, pathParams, config?)

api.delete<Path>(url, config?)
api.delete<Path>(url, pathParams, config?)

// head, options, trace — same shape as get/delete

POST, PUT, PATCH

api.post<Path>(url, body, config?)
api.post<Path>(url, body, pathParams, config?)

// put and patch — same shape as post

Real-world example

Path-based when you need it. In production code, reach for api.op instead — less string, more intent.

User service (path-based)
import { api } from "./api/api.client";

export async function getUser(id: string) {
  const { data, error } = await api.get("/users/{id}", { id });

  if (error) {
    if (error.code === "NOT_FOUND") return null;
    throw new Error(error.message);
  }

  return data;
}

export async function createUser(input: { name: string; email: string }) {
  const { data, error } = await api.post("/users", input);

  if (error) {
    if (error.code === "VALIDATION_ERROR") {
      throw new Error(`Validation failed: ${error.message}`);
    }
    throw new Error(error.message);
  }

  return data;
}

export async function listUsers(options?: { limit?: number; offset?: number }) {
  const { data, error } = await api.get("/users", { params: options });

  if (error) throw new Error(error.message);

  return data;
}

Next Steps

On this page