Schema definition
A use-q schema is a plain object literal where each key is a route name and each value is a RouteDefinition. The whole map is typed as as const satisfies Schema so TypeScript can infer every parameter and response down to the field level.
import type { Schema } from "@use-q/api-client";
export const schema = { // <route name>: <RouteDefinition>} as const satisfies Schema;Below is a tour of every field a RouteDefinition supports, using the running facilities → posts → comments example.
method and path
Section titled “method and path”method is one of "GET" | "POST" | "PUT" | "PATCH" | "DELETE". path is a literal string with {paramName} placeholders.
{ method: "GET", path: "/facilities/{facilityId}/posts/{postId}",}Path placeholders must match keys in pathParams. If you write /x/{foo} but pathParams doesn’t include foo, the type system will flag it at the useQ call site.
pathParams
Section titled “pathParams”A type-only hint for the path placeholders. Use the {} as { … } trick to declare the shape without paying any runtime cost.
listComments: { method: "GET", path: "/facilities/{facilityId}/posts/{postId}/comments", pathParams: {} as { facilityId: string; postId: string }, response: {} as Comment[],}At the call site, all keys are required:
useQ("listComments", { pathParams: { facilityId: "f1", postId: "p1" },});searchParams
Section titled “searchParams”Optional. Describes the query-string shape. Optional properties (search?:) become optional at the call site, and undefined values are stripped from the URL.
listPosts: { method: "GET", path: "/facilities/{facilityId}/posts", pathParams: {} as { facilityId: string }, searchParams: {} as { search?: string; tag?: string; limit?: number; sort?: "newest" | "oldest"; }, response: {} as Post[],}The resolved query string becomes part of the query key, so two requests with different searchParams get independent cache entries.
For mutating methods. Strongly typed; use-q will call JSON.stringify for you and set Content-Type: application/json.
createPost: { method: "POST", path: "/facilities/{facilityId}/posts", pathParams: {} as { facilityId: string }, body: {} as { title: string; body: string; tags?: string[] }, response: {} as Post,}response
Section titled “response”The success-response shape. This is the type of data from useQ and the resolved value of useM(...).mutateAsync(...).
response: {} as Post,// orresponse: {} as { items: Post[]; total: number },// orresponse: {} as void, // 204 No ContentAn array of TagValue objects that label what a query route reads. A TagValue is { type: string; [k: string]: string | number | ((params) => string | number) }. Functions receive the resolved path + search params.
listPosts: { // … tags: [{ type: "post", facilityId: (p) => p.facilityId }],},getPost: { // … tags: [ { type: "post", id: (p) => p.postId, facilityId: (p) => p.facilityId }, ],},Static vs dynamic tags
Section titled “Static vs dynamic tags”A static tag has only string/number values and matches every query for that route:
listSettings: { // … tags: [{ type: "settings" }],}A dynamic tag uses a function to derive a value from params, so it only matches queries with matching params:
tags: [{ type: "post", facilityId: (p) => p.facilityId }];A mutation that invalidates { type: "post", facilityId: "f1" } will:
- match every query whose
tagscontain{ type: "post" }without afacilityIdfilter (static), - match queries with
{ type: "post", facilityId: "f1" }, - not match queries with
{ type: "post", facilityId: "f2" }.
invalidatesTags
Section titled “invalidatesTags”The mirror of tags, but for mutations: what data does this route write? Listed tags are invalidated in onSettled.
createPost: { // … invalidatesTags: [{ type: "post", facilityId: (p) => p.facilityId }],},deletePost: { // … invalidatesTags: [ { type: "post", facilityId: (p) => p.facilityId }, { type: "post", id: (p) => p.postId, facilityId: (p) => p.facilityId }, ],},pagination
Section titled “pagination”For routes that return paginated data. The shape tells useInfiniteQ how to derive pageParam and getNextPageParam.
Page-number pagination
Section titled “Page-number pagination”listPosts: { method: "GET", path: "/facilities/{facilityId}/posts", pathParams: {} as { facilityId: string }, searchParams: {} as { page?: number; limit?: number }, response: {} as { items: Post[]; total: number }, pagination: { kind: "page-number", pageParam: "page", itemsField: "items", totalField: "total", },}Cursor pagination
Section titled “Cursor pagination”listFeed: { method: "GET", path: "/feed", searchParams: {} as { cursor?: string; limit?: number }, response: {} as { items: Post[]; nextCursor: string | null }, pagination: { kind: "cursor", cursorParam: "cursor", itemsField: "items", nextCursorField: "nextCursor", },}See useInfiniteQ for the consuming side.
Putting it together
Section titled “Putting it together”import type { Schema } from "@use-q/api-client";
interface Post { id: string; facilityId: string; title: string; body: string;}
interface Comment { id: string; postId: string; author: string; body: string;}
export const schema = { listPosts: { method: "GET", path: "/facilities/{facilityId}/posts", pathParams: {} as { facilityId: string }, searchParams: {} as { search?: string }, response: {} as Post[], tags: [{ type: "post", facilityId: (p) => p.facilityId }], }, getPost: { method: "GET", path: "/facilities/{facilityId}/posts/{postId}", pathParams: {} as { facilityId: string; postId: string }, response: {} as Post, tags: [ { type: "post", id: (p) => p.postId, facilityId: (p) => p.facilityId }, ], }, createPost: { method: "POST", path: "/facilities/{facilityId}/posts", pathParams: {} as { facilityId: string }, body: {} as { title: string; body: string }, response: {} as Post, invalidatesTags: [{ type: "post", facilityId: (p) => p.facilityId }], }, updatePost: { method: "PATCH", path: "/facilities/{facilityId}/posts/{postId}", pathParams: {} as { facilityId: string; postId: string }, body: {} as Partial<Pick<Post, "title" | "body">>, response: {} as Post, invalidatesTags: [ { type: "post", id: (p) => p.postId, facilityId: (p) => p.facilityId }, { type: "post", facilityId: (p) => p.facilityId }, ], }, deletePost: { method: "DELETE", path: "/facilities/{facilityId}/posts/{postId}", pathParams: {} as { facilityId: string; postId: string }, response: {} as void, invalidatesTags: [{ type: "post", facilityId: (p) => p.facilityId }], }, listComments: { method: "GET", path: "/facilities/{facilityId}/posts/{postId}/comments", pathParams: {} as { facilityId: string; postId: string }, response: {} as Comment[], tags: [{ type: "comment", postId: (p) => p.postId }], },} as const satisfies Schema;- Keep your schema in a shared package if you have a monorepo — see Monorepo usage.
- Don’t forget
as const satisfies Schema— withoutas const, literal types widen tostringand inference breaks. - Use the codegen CLI to generate a starting schema from an OpenAPI spec, then hand-edit tags.