Skip to content

createApiClient

createApiClient is the entry point for the React layer. Given a Schema, it returns a bundle of hooks and helpers that share one QueryClient, one Fetcher, and one TagRegistry.

import { createApiClient } from "@use-q/api-client-react";
import type { Schema } from "@use-q/api-client";
import { schema } from "./schema";
export const api = createApiClient<typeof schema>(schema, {
baseUrl: "https://api.example.com",
});

createApiClient accepts everything createFetcher does, plus a few React-specific knobs:

interface CreateApiClientOptions<TParsedError = unknown> {
baseUrl: string;
fetchFn?: typeof fetch;
defaultHeaders?:
| Record<string, string>
| (() => Promise<Record<string, string>> | Record<string, string>);
parseError?: (
res: Response,
body: unknown,
) => TParsedError | Promise<TParsedError>;
onError?: (
err: ApiError<TParsedError>,
ctx: { routeName: string; method: string; url: string; status: number },
) => void;
queryClient?: QueryClient;
}

See Core options for the fetcher-side fields. The React-only queryClient is documented in Bring your own QueryClient.

interface ApiClient<TSchema extends Schema, TParsedError = unknown> {
// Hooks
useQ: UseQ<TSchema, TParsedError>;
useM: UseM<TSchema, TParsedError>;
useInfiniteQ: UseInfiniteQ<TSchema, TParsedError>;
useSuspenseQ: UseSuspenseQ<TSchema, TParsedError>;
useQClient: () => QClient<TSchema>;
// Plumbing
fetcher: FetcherInstance<TSchema, TParsedError>;
queryClient: QueryClient;
queryKeys: QueryKeyFactory<TSchema>;
isApiError: typeof isApiError;
// Internal — exposed for advanced cases
_tagRegistry: TagRegistry;
}

The hook references are stable across renders — they’re created once in the closure, so it’s safe to destructure them at module scope:

export const { useQ, useM, useInfiniteQ, useQClient } = api;

Create the client in one place and re-export hooks so consumers never see the bare api.useQ(...) form:

src/api/client.ts
import { createApiClient } from "@use-q/api-client-react";
import { schema } from "./schema";
export const api = createApiClient<typeof schema>(schema, {
baseUrl: import.meta.env.VITE_API_BASE_URL,
defaultHeaders: () => ({
Authorization: `Bearer ${tokenStore.get() ?? ""}`,
}),
onError: (err) => {
if (err.status === 401) tokenStore.clear();
},
});
export const {
useQ,
useM,
useInfiniteQ,
useSuspenseQ,
useQClient,
queryClient,
} = api;

Components then read hooks with zero ceremony:

import { useQ } from "@/api/client";
function PostList() {
const { data } = useQ("listPosts", { pathParams: { facilityId: "f1" } });
return <ul>{data?.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

createApiClient constructs its own QueryClient, which you pass to QueryClientProvider:

import { QueryClientProvider } from "@tanstack/react-query";
import { api } from "@/api/client";
export function Root({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={api.queryClient}>
{children}
</QueryClientProvider>
);
}

If you’d rather share a QueryClient with something else (e.g. another createApiClient instance, or a non-use-q query), pass it via options.queryClient:

import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, refetchOnWindowFocus: false },
},
});
const api = createApiClient<typeof schema>(schema, {
baseUrl: "https://api.example.com",
queryClient,
});

See BYO QueryClient for the full discussion.

Pass the parsed-error type as the second generic:

interface ApiProblem {
type: string;
title: string;
detail: string;
}
export const api = createApiClient<typeof schema, ApiProblem>(schema, {
baseUrl: "https://api.example.com",
parseError: async (res) => {
const json = (await res.json().catch(() => null)) as ApiProblem | null;
return json ?? { type: "about:blank", title: res.statusText, detail: "" };
},
});

Now every hook’s error.parsed is typed as ApiProblem.

You can call createApiClient more than once per app — useful for unrelated APIs (e.g. internal API + public API, or distinct microservices):

export const internal = createApiClient<typeof internalSchema>(internalSchema, {
baseUrl: "https://internal.example.com",
queryClient,
});
export const public_ = createApiClient<typeof publicSchema>(publicSchema, {
baseUrl: "https://public.example.com",
queryClient,
});