Skip to content

use-q

A type-safe, schema-driven API client for TypeScript and React, built on TanStack Query v5.

use-q is a thin, opinionated layer that turns an OpenAPI-style schema into fully typed, cache-aware React hooks — without giving up the power of TanStack Query.

End-to-end type safety

Path params, search params, request bodies, response shapes, and error payloads are all inferred from a single RouteDefinition map. No any leaks into your components.

Framework-agnostic core

@use-q/api-client is a tiny zero-dependency fetcher that runs in Node, Bun, Deno, edge workers, or the browser. Use it from server actions, loaders, or scripts — no React required.

Schema-driven invalidation

Tag routes with tags/invalidatesTags and the React layer takes care of the rest. Mutations automatically refresh the right queries through a shared TagRegistry.

Optimistic updates that compose

Update multiple caches per mutation with snapshot/rollback semantics built in. Combine with additionalInvalidatesTags for arbitrary fan-out.

OpenAPI codegen

Generate a typed schema directly from an OpenAPI 3.x spec with use-q-codegen. Pagination is detected automatically.

Bring your own QueryClient

Reuse a single QueryClient across multiple createApiClient instances, or hook into persistence and devtools — use-q never gets in the way.

import { createApiClient } from "@use-q/api-client-react";
const schema = {
listPosts: {
method: "GET",
path: "/facilities/{facilityId}/posts",
pathParams: {} as { facilityId: string },
response: {} as { id: string; title: string }[],
tags: [{ type: "post", facilityId: (p) => p.facilityId }],
},
createPost: {
method: "POST",
path: "/facilities/{facilityId}/posts",
pathParams: {} as { facilityId: string },
body: {} as { title: string },
response: {} as { id: string; title: string },
invalidatesTags: [{ type: "post", facilityId: (p) => p.facilityId }],
},
} as const;
export const api = createApiClient<typeof schema>(schema, {
baseUrl: "https://api.example.com",
});
export const { useQ, useM, useQClient } = api;

Then in a component:

function PostList({ facilityId }: { facilityId: string }) {
const { data, isLoading } = api.useQ("listPosts", { pathParams: { facilityId } });
const createPost = api.useM("createPost", { pathParams: { facilityId } });
if (isLoading) return <p>Loading…</p>;
return (
<>
{data?.map((p) => <article key={p.id}>{p.title}</article>)}
<button onClick={() => createPost.mutate({ body: { title: "New post" } })}>
Add
</button>
</>
);
}

That’s it — list refetches automatically after the mutation thanks to schema-defined tags.