Skip to content

RouteDefinition

A RouteDefinition describes one route in your schema. A whole Schema is Record<string, RouteDefinition>.

type RouteDefinition<
TMethod extends HttpMethod = HttpMethod,
TPath extends string = string,
TPathParams = unknown,
TSearchParams = unknown,
TBody = unknown,
TResponse = unknown,
> = {
method: TMethod;
path: TPath;
pathParams?: TPathParams;
searchParams?: TSearchParams;
body?: TBody;
response: TResponse;
tags?: readonly TagValue<ResolvedParams<TPath, TPathParams, TSearchParams>>[];
invalidatesTags?: readonly TagValue<
ResolvedParams<TPath, TPathParams, TSearchParams>
>[];
pagination?: PaginationHint;
};
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type ResolvedParams<TPath, TPathParams, TSearchParams> = {
pathParams: TPathParams;
searchParams: TSearchParams;
};

The HTTP method. One of "GET" | "POST" | "PUT" | "PATCH" | "DELETE".

MethodCached as a query?Allowed for useM?Allowed for useQ?
GETyesnoyes
POSTnoyesno
PUTnoyesno
PATCHnoyesno
DELETEnoyesno

The type system enforces this — useQ only accepts routes with method: "GET".

A literal string with {paramName} placeholders. The placeholders must match pathParams keys.

{
method: "GET",
path: "/facilities/{facilityId}/posts/{postId}",
pathParams: {} as { facilityId: string; postId: string },
}

A type-only declaration of the path parameters. Use {} as { … } to avoid a runtime cost:

pathParams: {} as { facilityId: string; postId: string };

All keys are required at the call site. To make a param optional, model it as string | undefined:

pathParams: {} as { facilityId: string; previewToken?: string };

Optional. Same {} as { … } pattern; optional properties become optional at the call site.

searchParams: {} as { search?: string; page?: number };

undefined values are stripped before request and query-key construction, so they don’t fragment the cache.

Optional, for POST/PUT/PATCH/DELETE. use-q sets Content-Type: application/json and JSON.stringifys the value:

body: {} as { title: string; body: string };

For non-JSON bodies (form data, blobs), serialize yourself and pass a headers override per-call.

Required. The successful-response type. Use void for 204 No Content responses:

response: {} as Post; // 200 with JSON
response: {} as Post[]; // list response
response: {} as void; // 204
response: {} as { items: Post[]; total: number }; // paginated

A readonly TagValue[]. Each entry labels the cache entry produced by this route (only meaningful for GET routes).

tags: [{ type: "post", facilityId: (p) => p.facilityId }];

See Tag invalidation for the full lifecycle.

A readonly TagValue[]. For mutating routes, what tags should be invalidated when the mutation runs (in onSettled).

invalidatesTags: [{ type: "post", facilityId: (p) => p.facilityId }];

A PaginationHint (see below) for paginated routes. Required for useInfiniteQ:

pagination: {
kind: "page-number",
pageParam: "page",
itemsField: "items",
totalField: "total",
};
type TagValue<TParams = unknown> = {
type: string;
[k: string]: string | number | ((params: TParams) => string | number);
};

The type field is always a static string. Other fields can be:

  • Static: string | number literal value.
  • Dynamic: a function (params) => string | number where params is the resolved { pathParams, searchParams } from the call site.

Examples:

// Static
{ type: "settings" }
// Mixed
{ type: "post", facilityId: (p) => p.facilityId }
// Multiple dynamic fields
{ type: "comment",
postId: (p) => p.postId,
facilityId:(p) => p.facilityId,
}

See Tag invalidation > Static vs dynamic tags for matching semantics.

Two variants — page-number and cursor:

type PaginationHint =
| {
kind: "page-number";
pageParam: string; // searchParams key for the page number
itemsField: string; // response field containing items
totalField: string; // response field containing total count
}
| {
kind: "cursor";
cursorParam: string; // searchParams key for the cursor
itemsField: string; // response field containing items
nextCursorField: string; // response field containing next cursor
};
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",
},
}

@use-q/api-client exports utility types you can use to introspect a schema:

import type {
Schema,
RouteKey,
ReadableRouteKey,
MutatingRouteKey,
PaginatedRouteKey,
RouteParams,
RouteResponse,
} from "@use-q/api-client";
type AllRoutes = RouteKey<typeof schema>; // string union of every route name
type Reads = ReadableRouteKey<typeof schema>; // GET-only routes
type Writes = MutatingRouteKey<typeof schema>; // non-GET routes
type Paginated = PaginatedRouteKey<typeof schema>; // routes with a `pagination` block
type ListPostsParams = RouteParams<typeof schema, "listPosts">;
type ListPostsResponse = RouteResponse<typeof schema, "listPosts">;

These are how useQ, useM, and friends produce their fully-typed call-site shapes.

import type { Schema } from "@use-q/api-client";
interface Post {
id: string;
facilityId: string;
title: string;
body: string;
createdAt: string;
}
export const schema = {
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",
},
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", facilityId: (p) => p.facilityId, id: (p) => p.postId },
],
},
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 }],
},
} as const satisfies Schema;