Client SDK
@timbl/client is a framework-agnostic REST client built on fetch. It infers types from your content.config.ts so frontend code is typed end-to-end without bundling the runtime.
Setup
import { createTimblClient } from "@timbl/client";import type { InferCMS } from "@timbl/core";import type cms from "../../content.config";
type CMS = InferCMS<typeof cms>;
export const client = createTimblClient<CMS>({ baseUrl: "http://localhost:3000",});The config is imported as a type-only value. The @timbl/core runtime is never bundled into your frontend. The client ships zero runtime dependencies.
Collections
findMany(query)
List entries. query maps to HTTP query parameters.
const posts = await client.collection("posts").findMany({ status: "published", sort: "-date", limit: 10, offset: 0, q: "hello", include: ["tags"], depth: 1, select: ["title", "slug"],});// posts: Post[] (typed from your schema)// [{ id: "abc123", title: "Hello", slug: "hello", body: "...", bodyHtml: "...", status: "published", tags: [{...}], ... }]Returns an array of entries with the fields you requested. When include is set, relation fields are replaced with the full related documents.
findBySlug(slug, query)
Get one entry by slug.
const post = await client.collection("posts").findBySlug("hello", { include: ["tags", "author"], depth: 2,});// post: Post | null// { id: "abc123", title: "Hello", slug: "hello", body: "...", bodyHtml: "...", tags: [{...}], author: {...} }Returns null when no entry matches. The runtime throws TimblHttpError with code: "NOT_FOUND" only for server-side errors; a missing slug returns null on the client.
create(data)
Create an entry. Requires a session.
const created = await client.collection("posts").create({ title: "Hello", slug: "hello", body: "# Hello", status: "published",});// created: Post (with auto-generated id, createdAt, updatedAt, bodyHtml)// { id: "abc123", title: "Hello", slug: "hello", body: "# Hello", bodyHtml: "<h1>Hello</h1>", status: "published", createdAt: "...", updatedAt: "..." }update(id, data, options?)
Update an entry by id. Requires a session. Pass ifMatch for optimistic concurrency.
const updated = await client.collection("posts").update( "abc123", { title: "Updated" }, { ifMatch: 'W/"..."' },);delete(id)
Delete an entry by id. Requires a session.
await client.collection("posts").delete("abc123");Globals
get()
Read a singleton global.
const settings = await client.global("siteSettings").get();// settings: SiteSettings (typed from your global schema)// { siteName: "My Site", siteUrl: "https://example.com" }set(data, options?)
Write a singleton global. Requires a session.
await client.global("siteSettings").set({ siteName: "My Site" });Assets
findMany(query)
List assets. Public by default.
const assets = await client.assets.findMany({ limit: 20 });findById(id)
Get one asset by id.
const asset = await client.assets.findById("asset_123");upload({ file, filename?, directory?, alt? })
Upload a file. Requires a session. Pass a File object (browser) or a Blob.
const uploaded = await client.assets.upload({ file: fileInput.files[0], directory: "posts", filename: "cover.jpg", alt: "Cover image",});// uploaded: StoredFile// { id: "asset_123", filename: "cover.jpg", originalName: "photo.jpg", mimeType: "image/jpeg", size: 524288, path: "posts/cover.jpg", url: "/uploads/posts/cover.jpg" }delete(id)
Delete an asset by id. Requires a session.
await client.assets.delete("asset_123");Auth
session()
Check the current session.
const { authenticated, principal, identity } = await client.auth.session();// { authenticated: true, principal: { id: "user_123", email: "editor@example.com" }, identity: { provider: "better-auth", subject: "user_123", actorType: "human", ... } }// or when unauthenticated: { authenticated: false, principal: null, identity: null }action(action, body)
Invoke a canonical auth action (login, register, logout, or a custom action advertised by the adapter).
await client.auth.action("login", { email: "editor@example.com", password: "..." });System
health()
Call GET /api/health. Returns process health status.
ready()
Call GET /api/ready. Returns 503 when a dependency (database) is not ready.
openApi()
Fetch the generated OpenAPI document at /openapi.json.
exportContent()
Call GET /api/export. Available only when the runtime is started with ENABLE_CMS_EXPORT=true. Requires an authenticated service principal.
Error handling
Non-2xx responses throw TimblHttpError. Inspect the code property to decide how to react.
import { TimblHttpError } from "@timbl/client";
try { await client.collection("posts").findBySlug("missing");} catch (error) { if (error instanceof TimblHttpError && error.code === "NOT_FOUND") { // render a 404 page } else { throw error; }}The code values align with @timbl/core ErrorCodes. See Error Codes for the full list and HTTP status mapping.
Query parameters
The query object on findMany and findBySlug accepts:
| Param | Type | Purpose |
|---|---|---|
limit | number | Max entries to return |
offset | number | Skip entries for pagination |
sort | string | Sort field, prefix with - for descending |
q | string | Full-text search query |
status | string | Filter by status field |
select | string[] | Project specific fields |
include | string[] | Expand relation fields |
depth | number | Relation expansion depth |
locale | string | Request a specific locale |
fallbackLocale | string | Fallback locale when the requested one is missing |
What the client does not expose
The client deliberately does not expose private runtime modules, Hono types, Drizzle types, or direct adapter handles. If you need those, you are writing a plugin or an adapter, not a frontend. See Plugins and Custom Adapters.
See also
- HTTP API Reference: the endpoints the client calls
- Error Codes: the
codevalues onTimblHttpError - Quick Start: install and first query
- Astro Kitchen Sink: full client usage in an Astro app
- Next.js Kitchen Sink: full client usage in a Next.js app