Client SDK
One typed object that calls your app over HTTP. Collections, globals, routes, search, channels and realtime, with every argument and return shape inferred from the AppConfig codegen wrote.
A wrong collection name is a compile error here. So is a wrong where field,
and so is a key on create that the collection does not have. The server
still checks access and validates the value it receives.
What is on the client
createClient<AppConfig>(config) returns the members below. It also returns
crdt, which throws the moment you read it.
| Member | What it is |
|---|---|
collections | Typed CRUD, one object per collection |
globals | get and update for each singleton |
routes | Your own endpoints, reached through a nested proxy |
search | search() and reindex() against the search adapter |
realtime | The subscription API that live() is built on |
channels | Typed application event streams |
setLocale | Sets the accept-language header on later requests |
getLocale | Reads that locale back |
getBasePath | Reads the normalized base path back |
`client.crdt` throws when you read it
It was removed so 164 KB of CRDT code stays out of bundles that never open a
document. Build it from the client you already have with
createCrdtClient(client), imported from questpie/crdt. See Collaborative
documents.
Create it
Create the client once and export it. createClient comes from
questpie/client. AppConfig is the type codegen wrote, and it is what makes
every call typed.
import { createClient } from "questpie/client";
import type { AppConfig } from "#questpie";
export const client = createClient<AppConfig>({
baseURL:
typeof window !== "undefined"
? window.location.origin
: process.env.APP_URL || "http://localhost:3000",
basePath: "/api",
});Then call it from anywhere. The same client runs in a browser, a Bun script, a worker or a test.
import { client } from "@/lib/client.js";
const { docs, totalDocs } = await client.collections.posts.find({
where: { published: true },
limit: 10,
});AppConfig is a type parameter, not a value. Run questpie generate after a
schema change and every method moves with it. TypeScript checks the names, and
the server still checks access and validates the input.
Configuration
| Option | Default | What it does |
|---|---|---|
baseURL (required) | none | Origin the client calls. Joined with basePath to build every URL. |
basePath | "/" | Where the server handler is mounted. It has to match. |
fetch | globalThis.fetch | Your own fetch. An instrumented one, or a polyfill. |
headers | {} | Headers merged into every request. |
getAuthHeaders | none | Runs before each request and returns headers. Beats headers on a conflict. |
useSuperJSON | true | Sends and parses SuperJSON, so Date, Map, Set and BigInt survive. |
crdt | none | Runtime config for collaborative documents. |
Every request sends credentials: "include", so a session cookie travels with
it. Reach for getAuthHeaders when the token rotates, such as a bearer token
in a mobile app. It runs fresh on each request, so you never rebuild the
client. basePath gains a leading slash if you left it off, and loses a
trailing one.
`basePath` must match the server, and `/` reads back as `''`
The client's basePath and the handler's basePath have to be identical or
every request 404s. Use "/api" for an app that also serves a frontend.
getBasePath() returns the normalized value, which is "" when you passed
"/".
Collection methods
client.collections.<name> carries the methods below. Reads take the full
query language. Mutations take a second argument of locale, localeFallback
and stage, which goes into the query string. The uploads and the two
introspection calls are the exceptions.
| Method | HTTP | Returns |
|---|---|---|
find(options?) | GET /:collection | a page of rows |
findOne(options?) | GET /:collection/:id, else find at limit 1 | row or null |
count(options?) | GET /:collection/count | number |
create(data, options?) | POST /:collection | row |
updateById({ id, data }, options?) | PATCH /:collection/:id | row |
deleteById({ id }, options?) | DELETE /:collection/:id | { success, data } |
restoreById({ id }, options?) | POST /:collection/:id/restore | row |
purgeById({ id }, options?) | POST /:collection/:id/purge | { success: true } |
updateMany({ where, data }, options?) | PATCH /:collection | row[], claim-checked |
updateBatch({ updates }, options?) | POST /:collection/update-batch | row[] |
deleteMany({ where }, options?) | POST /:collection/delete-many | { success, count } |
findVersions({ id, limit?, offset? }, options?) | GET /:collection/:id/versions | rows plus version metadata |
revertToVersion({ id, version?, versionId? }, options?) | POST /:collection/:id/revert | row |
transitionStage({ id, stage, scheduledAt? }, options?) | POST /:collection/:id/transition | row |
upload(file, options?) | POST /:collection/upload | created row |
uploadMany(files, options?) | POST /:collection/upload, one file at a time | row[] |
meta() | GET /:collection/meta | CollectionMeta |
schema() | GET /:collection/schema | CollectionSchema |
live(options, onSnapshot, opts?) | realtime | an unsubscribe function |
liveIter(options?, opts?) | realtime | AsyncGenerator of the same snapshots |
update, delete and restore are aliases of the three …ById methods, so on
the client they act on one row. The server gives those same names to its bulk
operations, so prefer the …ById names. purgeById appears only on
collections with soft delete on. Every collection types upload, but the
server answers 400 unless the collection called .upload().
Global methods
A global is one row with no id, so client.globals.<name> is a shorter list.
| Method | HTTP | Notes |
|---|---|---|
get(options?) | GET /globals/:global | with, columns, locale and stage |
update(data, options?) | PATCH /globals/:global | data first, query options second |
meta() and schema() | GET /globals/:global/meta, /schema | introspection |
findVersions(options?) | GET /globals/:global/versions | one options object, no separate params |
revertToVersion({ id?, version?, versionId? }, options?) | POST /globals/:global/revert | params in the body |
transitionStage({ stage, scheduledAt? }, options?) | POST /globals/:global/transition | no id |
live(options, onSnapshot, opts?) | realtime | options are with and locale only |
liveIter(options?, opts?) | realtime | the generator form |
update takes the data object directly, not the { id, data } wrapper that
collections use.
Routes and search
Your routes arrive as a nested proxy. Each folder is one
dot, and the HTTP method is the leaf that sends the request. client.search
holds two methods and talks to your search adapter.
// routes/admin/stats.post.ts
const stats = await client.routes.admin.stats.post({ period: "week" });
const results = await client.search.search({ query: "questpie", limit: 20 });
await client.search.reindex("posts");Route leaves are get, post, put, patch and delete. A get turns its
input into the query string. Any other property is one more path segment,
camelCase to kebab-case, so adminStats requests admin-stats. Search hands
back { docs, total, facets? }, each doc carrying _collection and _search.
Locale
setLocale rewrites the client's shared headers in place, so it moves every
later request on that client. It does not reach a subscription that is already
open, because live() builds its topic when you call it. Pass locale in the
live options instead.
client.setLocale?.("de"); // sends accept-language: de
client.getLocale?.(); // "de"
client.setLocale?.(); // pass nothing to clear itAll three are typed optional on QuestpieClient, so call them with ?.().
Per-call overrides go in the method options instead, such as
find({ locale: "de" }). See Client i18n.
Where each topic lives
| Topic | Page |
|---|---|
| Every collection method, with examples | Collection methods |
| Sending files, progress and cancellation | Uploads |
| Every global method | Global methods |
| What is thrown, and how to read a field error | Errors |
| Mounting the handler the client calls | Framework adapters |
live(), liveIter() and the raw subscribe API | Realtime |
| Typed events, presence and publishing | Channels |
| React hooks over this client | TanStack Query |
Which search adapter answers client.search | Search |
revision and expectedRevision | Optimistic concurrency |
Next
Collection methods is the same list again, with a runnable call for each one.