Import
import { createQueryCaller } from "@noverax/courier-react"Direct vs Proxy mode
This is the direct-mode query caller — it accepts the API definition object and connects straight to api.options.baseURL. For proxy mode (browser → server adapter → backend), use createProxyQueryCaller.
When to use direct mode
Use createQueryCaller(api) when:
- You're building an SPA (Vite, Create React App) that talks to a separate backend
- You're building a React Native app
- You're building an internal tool where bundle size doesn't matter
- The API definition can safely ship to the client (no secrets, no auth tokens hard-coded)
Use createProxyQueryCaller instead when:
- You need to hide secrets (auth headers, API keys) from the browser
- You need CORS bypass (browser → your server → third-party API)
- You want to keep the API definition server-only to reduce client bundle size
- You want request logging / observability through a single proxy endpoint
Signature
declare function createQueryCaller<TApi extends DefinedApi>(
api: TApi,
defaults?: QueryCallerDefaults,
): QueryCallerClient<TApi>The first argument is the API definition object itself (returned by defineApi()). No string source name, no generic parameter — TypeScript infers everything from the api object.
Defaults
type QueryCallerDefaults = {
timeout?: number
headers?: Record<string, string>
context?: RequestContext
onError?: (error: HTTPError) => void | Promise<void>
onSuccess?: (data: unknown) => void | Promise<void>
ws?: CallerWSOptions // as createCaller's
}ws sets up the sockets useChannel opens: the token, the reconnect backoff, the heartbeat and the queue limits — see Connection options.
No proxyPath or credentials — all transports use the baseURL from the API definition directly.
Setup
import { createQueryCaller, createQueryKeys } from "@noverax/courier-react"
import { api } from "@/shared/api"
export const q = createQueryCaller(api, {
timeout: 10000,
headers: { "X-Client": "web-spa" },
})
export const queryKeys = createQueryKeys(api)Hooks
All hooks are identical to createProxyQueryCaller. The only difference is that requests bypass the proxy adapter and connect directly to api.options.baseURL.
"use client"
import { q } from "@/lib/query"
export function UserList() {
const { data, isLoading } = q.users.useQuery("getAll")
if (isLoading) return <p>Loading...</p>
return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}Variables, mutations, infinite queries
// Query with variables
const { data: user } = q.users.useQuery("getById", { variables: { id: "1" } })
// Mutation
const { mutate } = q.users.useMutation("create")
mutate({ name: "Alice" })
// Infinite query
const { data: pages, fetchNextPage } = q.posts.useInfiniteQuery("list", {
variables: (cursor) => ({ cursor }),
initialPageParam: 0,
getNextPageParam: (page) => page.nextCursor,
})SSE subscriptions
const { data, status, events } = q.notifications.useSubscription("feed", {
variables: { userId: "user-1" },
})The SSE connection goes directly to ${api.options.baseURL}/${groupPrefix}/${path} — no proxy hop. The path is the one the endpoint addressed, worked out from each call's variables: a :param in it is filled from the variable of that name, a .via() closure is run with the variables, and an endpoint that named no path is reached by its name. The variables the path did not take are sent in the query string.
WS channels
const { send, lastMessage, status } = q.chat.useChannel("room", {
variables: { roomId: "general" },
})
send({ text: "Hello" })The WebSocket connection goes directly to ${baseURL with ws:// scheme}/${groupPrefix}/${path} — no proxy hop. The path is worked out from the call's variables as it is for SSE; the variables themselves are all sent in the subscribe message.
A stream or channel served by this process with .implement() has no address at baseURL, which is the upstream's. useSubscription and useChannel report it through error and onError as ConfigurationError("DIRECT_CALL_TO_SERVED_ENDPOINT") and open nothing; reach it with createProxyQueryCaller.
Query Keys
Query keys are auto-generated as [sourceName, group, method, variables?] where sourceName comes from defineApi({ name }) (or is derived from the baseURL hostname when omitted).
import { createQueryKeys } from "@noverax/courier-react"
import { api } from "@/shared/api"
export const queryKeys = createQueryKeys(api)queryKeys("users") // ["api", "users"]
queryKeys("users", "getAll") // ["api", "users", "getAll"]
queryKeys("users", "getById", { id: 1 }) // ["api", "users", "getById", { id: 1 }]
queryClient.invalidateQueries({ queryKey: queryKeys("users") })See createQueryKeys for details.
Comparison
| Feature | createQueryCaller(api) | createProxyQueryCaller<T>("name") |
|---|---|---|
| Mode | Direct | Proxy |
| Argument | API object | Source name string + generic |
| Server adapter required | No | Yes (apiRoute, sseRoute, WS handler) |
| Ships API definition to client | Yes | No |
| Hides secrets in API definition | No | Yes |
| CORS bypass | No | Yes (proxy lives on your origin) |
| Best for | SPA, React Native, internal tools | Next.js / SSR / multi-tenant apps |
Related
createCaller— direct-mode caller without React Query hookscreateProxyQueryCaller— proxy-mode equivalentcreateQueryKeys— type-safe query key factory for direct mode- React Query Setup — full setup guide with QueryClientProvider