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
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 — goodResult-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 hereSee 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/deletePOST, PUT, PATCH
api.post<Path>(url, body, config?)
api.post<Path>(url, body, pathParams, config?)
// put and patch — same shape as postReal-world example
Path-based when you need it. In production code, reach for api.op instead — less string, more intent.
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;
}