Docs / Protocols
TypeScript client SDK
Generate a fully-typed, fluent TypeScript client alongside the API — selection-inferred return types, typed writes, aggregates, subscriptions and a TanStack Query binding.
gqlith can emit a fourth surface next to GraphQL, REST and MCP: a fully-typed TypeScript client SDK for your own frontend. The types come from the same schema the server was generated from, so the contract can’t drift — and like everything else gqlith emits, the SDK is plain TypeScript you own, with no runtime dependency on gqlith.
Enabled by setting targets.clientSdk: typescript.
Fluent reads, typed to the selection
const gq = createGqlithClient(fetcher);
const result = await gq.recipes
.where({ status: { eq: "published" }, servings: { gte: 4 } })
.orderBy(["createdAt_DESC"])
.page({ first: 10 })
.select({ id: true, title: true, authorUser: { id: true, name: true } });
The return type is narrowed to exactly what you selected — unselected fields are compile errors, nested relations are typed recursively. No hand-written GraphQL, no codegen watch step against a remote endpoint: the documents are assembled and validated at build time.
Single rows fetch by any unique key: gq.recipe({ id }).select({ … }). Reads also cover precise connection projection (edges, nodes, pageInfo, totalCount), limit / offset / distinctOn, nested-relation arguments, and the soft-delete selectors withDeleted / onlyDeleted.
Typed writes
await gq.recipes.create(data).select({ id: true });
await gq.recipes.update(where).set(data).expectVersion(3).select({ id: true });
All the write verbs are covered — create, createMany, update, updateMany, upsert, delete, restore — with optimistic concurrency surfaced as first-class calls: expectVersion(n) for version-column tables, expectUpdatedAt(ts) for timestamp locks. Bulk writes take a .atMost(n) cap that fails the whole operation with ATMOST_EXCEEDED rather than silently over-writing.
Batching, aggregates, escape hatches
- Multi-root batching —
gq.batch({ a: op1, b: op2 }).run()issues one GraphQL request with aliased roots; multiple mutations ride the server’s cross-mutation transaction, so both commit or both roll back. - Aggregates —
gq.users.aggregate({ where }).select({ count: true, sum: { id: true } }), plus grouped aggregates and inline nested-relation aggregates. - Custom SQL functions exposed as roots get their own typed accessors; so do the token-management mutations.
- FiltrQL escape hatch — pass the same filter string the REST and MCP surfaces accept.
Errors as data
Every call resolves to a GqlithResult — { ok, data, errors } — so partial success and structured error extensions are handled in the type system instead of thrown across your app.
Realtime and framework bindings
Typed SSE subscriptions ship with their own reader (gq.subscribe(SubscribeRecipeUpdated, vars) over an SSE-capable fetcher), and an optional TanStack Query binding emits queryOptions / mutationOptions / infiniteQueryOptions factories for any fluent operation — feed them straight into useQuery, useMutation and useInfiniteQuery.
Storage-enabled projects additionally get client.files.uploadFile / uploadFiles / url, plus crash-survivable large-file uploads: uploadFileResumable returns a JSON resume handle, resumeUpload picks up where it left off, and uploadFile switches to multipart automatically above the configured threshold — see File storage.