Signature
Access useQuery through the query caller's dot-notation API. Types are fully inferred from your endpoint definitions:
const result = q.users.useQuery("getAll", options)
// q.<group>.useQuery(method, options?)
// group — endpoint group name (e.g. "users"), or a group inside one: q.billing.invoices
// method — method name within the group (e.g. "getAll")
// options — { variables?, requestOptions?, ...reactQueryOptions }Basic query
The simplest usage — fetch data on mount. The data type is automatically inferred from the method's response type:
"use client"
import { q } from "@/lib/query"
export function UserList() {
const { data, isLoading, error } = q.users.useQuery("getAll")
// ^? UserList — auto-inferred from .response<UserList>()
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return (
<ul>
{data?.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}Query with variables
When a method defines variables (e.g. .variables<{ id: number }>()), the variables option becomes required. TypeScript will error if you omit it:
"use client"
import { q } from "@/lib/query"
export function UserProfile({ userId }: { userId: number }) {
// .variables<{ id: number }>() → variables is required
const { data: user, isLoading } = q.users.useQuery("getById", {
variables: { id: userId },
// ^? { id: number } — auto-inferred from the chain
})
if (isLoading) return <p>Loading...</p>
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
</div>
)
}Variables type safety
If a method never calls .variables(), the variables option is optional. If it declares a concrete variables type, the options object with variables is required.
Conditional queries
Use the enabled option to conditionally execute a query. The query won't fire until the condition is true:
"use client"
import { q } from "@/lib/query"
export function UserProfile({ userId }: { userId: number | null }) {
const { data, isLoading } = q.users.useQuery("getById", {
variables: { id: userId! },
// Only fetch when userId is available
enabled: userId !== null,
})
if (!userId) return <p>Select a user</p>
if (isLoading) return <p>Loading...</p>
return <h1>{data?.name}</h1>
}React Query options
All standard React Query options are supported alongside Courier-specific options. Configure caching, refetching, and data transformation:
const { data } = q.users.useQuery("getAll", {
variables: { page: 1, limit: 20 },
// Standard React Query options — all supported
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
refetchInterval: 30 * 1000, // Refetch every 30 seconds
refetchOnWindowFocus: false, // Don't refetch on tab focus
placeholderData: previousData, // Keep previous data while refetching
select: (data) => data.data, // Transform response before caching
// Courier-specific options
requestOptions: {
timeout: 5000, // The options .with() takes, per call
},
})Auto-generated query keys
Courier automatically generates hierarchical query keys from the source, group, method, and variables. This enables cache invalidation at different levels of specificity:
// Auto-generated query key: ["api", "users", "getAll"]
q.users.useQuery("getAll")
// With variables: ["api", "users", "getById", { id: 5 }]
q.users.useQuery("getById", { variables: { id: 5 } })
// Invalidate a specific method
queryClient.invalidateQueries({ queryKey: ["api", "users", "getAll"] })
// Invalidate all queries for a group
queryClient.invalidateQueries({ queryKey: ["api", "users"] })
// Invalidate all queries for the source
queryClient.invalidateQueries({ queryKey: ["api"] })Group-level invalidation
Unlike flat key formats, hierarchical keys let you invalidate all queries for a group at once. ["api", "users"] matches getAll, getById, create — everything in the users group.
Error handling
The error property is typed as Error, as TanStack Query types it, because not every failure is an HTTP one. It is an HTTPError for an error answer or a call that got no answer, a ValidationError when variables or the answer fail their schema, a ConfigurationError when the client is set up wrongly, the reason a call was cancelled with through your own signal, or whatever a select threw. The same holds for useSuspenseQuery, useInfiniteQuery, useMutation and useComputedQuery. Use isHTTPError to check if it's an HTTP error with status and response details, and isValidationError or isConfigurationError for the others:
"use client"
import { isHTTPError } from "@noverax/courier"
import { q } from "@/lib/query"
export function UserList() {
const { data, error, isError } = q.users.useQuery("getAll")
if (isError) {
// Check if it's an HTTP error with status/response
if (isHTTPError(error)) {
if (error.status === 404) return <p>Not found</p>
if (error.status === 403) return <p>Access denied</p>
}
return <p>Something went wrong: {error.message}</p>
}
return <ul>{data?.data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}Next steps
- useMutation — create, update, and delete data with typed mutations
- Advanced Patterns — prefetching, hydration, optimistic updates, and cache strategies
- Error Handling — deep dive into HTTPError and error recovery