Skip to content

TanStack Query

Generate typed query options, mutations, pagination, cache keys, and invalidation helpers.

Use this when React Query owns your server-state cache. The generated companion gives every operation a typed query key and fetch function, so components do not recreate either by hand.

Keep the three generated files together in config:

import { defineConfig } from "typed-openapi";
export default defineConfig({
input: "./openapi.yaml",
output: "./src/api/openapi.ts",
defaultFetcher: "api.client.ts",
tanstack: "query.ts",
});

Or generate the companion from the command line:

Terminal window
pnpm add @tanstack/react-query
pnpm exec typed-openapi openapi.yaml \
--output src/api/openapi.ts \
--default-fetcher \
--tanstack query.ts

The generated module is intentionally framework-light: it creates typed query and mutation options. You still own your QueryClient, hooks, cache policy, and component boundaries.

import { useQuery } from "@tanstack/react-query";
import { TanstackQueryApiClient } from "../api/query";
import { api } from "../api/api.client";
const queryApi = new TanstackQueryApiClient(api);
export function Pet({ petId }: { petId: number }) {
const query = useQuery(
queryApi.get("/pet/{petId}", { path: { petId } }).queryOptions,
);
return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
}

Construct TanstackQueryApiClient once per API client instance. The endpoint call returns a stable options object; pass that object to useQuery, server prefetching, or ensureQueryData instead of hand-writing keys and fetch functions.

The same generated call exposes suspenseQueryOptions for Suspense-based routes and works with fetchQuery() or ensureQueryData() outside React.

const options = queryApi.get("/pet/{petId}", { path: { petId: 7 } }).suspenseQueryOptions;
const pet = await queryClient.ensureQueryData(options);

mutationOptions contains a correctly typed mutationFn and status-aware error type.

import { useMutation } from "@tanstack/react-query";
const createPet = useMutation(
queryApi.mutation("post", "/pet", { withResponse: true }).mutationOptions,
);
createPet.mutate(
{ body: { id: 7, name: "Mochi" } },
{
onSuccess(result) {
if (result.ok) console.log(result.data.name);
else if (result.status === 400) console.error(result.data);
},
},
);

When an endpoint accepts a page parameter, opt into the generated infinite-query helper and name that parameter:

const feed = queryApi.get("/pets", { query: { limit: 20 } });
const options = feed.infiniteQueryOptions({
initialPageParam: 0,
pageParamKey: "page",
getNextPageParam: (lastPage) => lastPage.nextPage,
});

Use stable keys outside components too:

import { invalidateEndpoint, queryKeyFactory } from "../api/query";
await invalidateEndpoint(queryClient, "/pet/findByStatus", {
query: { status: "available" },
});
const key = queryKeyFactory.endpoint("/pet/findByStatus", {
query: { status: "available" },
});

Effect-native clients work here too: the generated wrapper executes their operations with Effect.runPromise.

Use invalidateEndpoint() or queryKeyFactory after a mutation. They encode the same path and parameter shape as the query, which prevents cache misses caused by manually assembled keys.