Skip to content

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 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.

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" },
});

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,
}

The success-response shape. This is the type of data from useQ and the resolved value of useM(...).mutateAsync(...).

response: {} as Post,
// or
response: {} as { items: Post[]; total: number },
// or
response: {} as void, // 204 No Content

An 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 },
],
},

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 tags contain { type: "post" } without a facilityId filter (static),
  • match queries with { type: "post", facilityId: "f1" },
  • not match queries with { type: "post", facilityId: "f2" }.

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 },
],
},

For routes that return paginated data. The shape tells useInfiniteQ how to derive pageParam and getNextPageParam.

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",
},
}
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.

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 — without as const, literal types widen to string and inference breaks.
  • Use the codegen CLI to generate a starting schema from an OpenAPI spec, then hand-edit tags.