@contract-first-api/react-query
v2.0.0
Published
Wrap a contract-first API client with React Query hooks and cache helpers.
Maintainers
Readme
@contract-first-api/react-query
@contract-first-api/react-query wraps an ApiClient tree with React Query helpers. It keeps the same contract structure, but gives each contract React Query-friendly methods like useQuery, useMutation, invalidate, and $fetch.
What you do with this package
Use it to:
- turn a typed API client into query and mutation hooks
- keep query keys aligned with the contract path
- invalidate or clear cache using the same contract tree
- inspect the original contract on each wrapped node through
$contract - post-process the generated adapter with
mapWrappedContracts
Basic setup
import { ApiClient } from "@contract-first-api/api-client";
import createAdapter from "@contract-first-api/react-query";
import { QueryClient } from "@tanstack/react-query";
import { contracts } from "@example/shared";
const client = new ApiClient({
baseUrl: "http://localhost:3001/api",
contracts,
});
export const queryClient = new QueryClient();
export const api = createAdapter(client.api, queryClient);How you use it in components
For GET contracts, use query hooks:
const health = api.health.get.useQuery();
const todos = api.todos.list.useQuery();For non-GET contracts, use mutations:
const createTodo = api.todos.create.useMutation({
onSuccess: async () => {
await api.todos.list.invalidate();
},
});
await createTodo.mutateAsync({ title: "New item" });Useful helpers on each contract
Wrapped contracts expose the original contract through $contract, direct calls through $fetch, and the React Query helpers that match the default behavior for that route:
$contractfor the original contract definition, includingmeta$fetchfor direct calls without hooks$tryFetchfor{ success, data | error }style handlinguseQueryanduseSuspenseQueryforGETroutesuseMutationfor non-GETroutesinvalidateto refresh cached queriesclearto remove cached queries$getKeyto get the query key for a requestsetDatato write into the cache$reactQueryApito access both query and mutation helpers during custom transforms
Examples:
await api.todos.list.invalidate();
const health = await api.health.get.$fetch();
const cachedHealthKey = api.health.get.$getKey();$fetch also forwards fetch options to the underlying API client:
await api.health.get.$fetch({ cache: "no-store" });
await api.todos.create.$fetch(
{ title: "Ship docs" },
{ credentials: "include" },
);Post-processing the wrapped tree
Use mapWrappedContracts when you want to build your own adapter layer on top of the generated one.
import createAdapter, {
mapWrappedContracts,
} from "@contract-first-api/react-query";
const baseApi = createAdapter(client.api, queryClient);
type ReactQueryMeta = {
reactQuery?: {
safe?: boolean;
};
};
const customApi = mapWrappedContracts<ReactQueryMeta, typeof client.api>(
baseApi,
(node) =>
node.$contract.meta?.reactQuery?.safe || node.$contract.method === "GET"
? node.$reactQueryApi
: node,
);That pattern is useful when contract metadata should influence how your app exposes or groups routes.
$reactQueryApi gives your transform access to the full helper surface even when the default adapter only exposes the query or mutation subset.
Practical flow
In a React app, the usual order is:
- Define contracts in shared code.
- Build an
ApiClientfrom those contracts. - Create a
QueryClient. - Wrap the client with
createAdapter. - Optionally post-process the wrapped tree with
mapWrappedContracts. - Use the generated contract helpers inside components.
If your app already uses React Query, this package makes the contract tree feel like a native part of that setup.
