Skip to content

API ReferencecreateQueryCaller

createQueryCaller

Creates a direct-mode React Query client that connects straight to the backend, no proxy adapter required.

3 min readdirect mode

Import

ts
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

ts
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

ts
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

lib/query.ts
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.

components/user-list.tsx
"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

ts
// 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

tsx
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

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

lib/query.ts
import { createQueryKeys } from "@noverax/courier-react"
import { api } from "@/shared/api"

export const queryKeys = createQueryKeys(api)
ts
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

FeaturecreateQueryCaller(api)createProxyQueryCaller<T>("name")
ModeDirectProxy
ArgumentAPI objectSource name string + generic
Server adapter requiredNoYes (apiRoute, sseRoute, WS handler)
Ships API definition to clientYesNo
Hides secrets in API definitionNoYes
CORS bypassNoYes (proxy lives on your origin)
Best forSPA, React Native, internal toolsNext.js / SSR / multi-tenant apps