# Client SDK (/docs/client/sdk)

---
title: Client SDK
description: 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.
kind: reference
package: questpie
---

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                   |

<Callout type="warn" title="`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](/docs/client/collaborative-documents).
</Callout>

## 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.

```ts title="src/lib/client.ts"
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.

```ts
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.

<Callout
	type="warn"
	title="`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
	`"/"`.
</Callout>

## 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](/docs/code/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.

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

```ts
client.setLocale?.("de"); // sends accept-language: de
client.getLocale?.(); // "de"
client.setLocale?.(); // pass nothing to clear it
```

All 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](/docs/client/i18n).

## Where each topic lives

| Topic                                            | Page                                                                      |
| ------------------------------------------------ | ------------------------------------------------------------------------- |
| Every collection method, with examples           | [Collection methods](/docs/client/sdk/collections)                        |
| Sending files, progress and cancellation         | [Uploads](/docs/client/sdk/uploads)                                       |
| Every global method                              | [Global methods](/docs/client/sdk/globals)                                |
| What is thrown, and how to read a field error    | [Errors](/docs/client/sdk/errors)                                         |
| Mounting the handler the client calls            | [Framework adapters](/docs/client/sdk/framework-adapters)                 |
| `live()`, `liveIter()` and the raw subscribe API | [Realtime](/docs/client/realtime)                                         |
| Typed events, presence and publishing            | [Channels](/docs/client/channels)                                         |
| React hooks over this client                     | [TanStack Query](/docs/client/tanstack-query)                             |
| Which search adapter answers `client.search`     | [Search](/docs/infrastructure/search)                                     |
| `revision` and `expectedRevision`                | [Optimistic concurrency](/docs/schema/collections/optimistic-concurrency) |

## Next

**[Collection methods](/docs/client/sdk/collections)** is the same list again,
with a runnable call for each one.
