Skip to content

@kyro-cms/connect

0.9.7+

@kyro-cms/connect is the official typed client SDK for Kyro CMS. It provides a fully typed API surface using generic TRouter and CollectionClient<T, F> types, with GraphQL and file upload support. Runs in Node.js, browsers, Deno, Bun, and edge runtimes.


Installation

bash
pnpm add @kyro-cms/connect

Quick Start

ts
import { createClient } from "@kyro-cms/connect";

const client = createClient({
  url: "http://localhost:4321",
  apiKey: "my-api-key",
});

const posts = await client.collection("posts").find();

Typed Client

Pass a generic TRouter type for end-to-end type safety. Generate it with kyro generate:types.

ts
import { createClient } from "@kyro-cms/connect";
import type { KyroAppRouter } from "./kyro.generated";

const client = createClient<KyroAppRouter>({
  url: "http://localhost:4321",
  apiKey: "my-api-key",
  credentials: "include", // default
});

Options

OptionTypeDefaultDescription
urlstringKyro CMS base URL
apiKeystringAPI key for server-side auth
credentialsstring"include"Credential mode for fetch requests

Automatic Base URL Resolution & Routing

createClient automatically normalizes the provided url (whether set to root http://localhost:4321, /api, or /api/trpc) so that both Typed Router procedures and REST Collection methods automatically route to their correct backend endpoints:

  • REST Collections (client.collection("posts")): Routes to ${rootUrl}/api/posts
  • Globals (client.global("settings")): Routes to ${rootUrl}/api/globals/settings
  • GraphQL (client.gql(...)): Routes to ${rootUrl}/api/graphql
  • Typed Router Procedures (client.posts.find()): Routes to ${rootUrl}/api/trpc/posts.find

Whether your environment variable PUBLIC_KYRO_URL is configured as http://localhost:4321, http://localhost:4321/api, or http://localhost:4321/api/trpc, the client handles subpath resolution automatically.


Comparison: Typed Router API vs. REST Collection API

@kyro-cms/connect provides two distinct call patterns for querying data from Kyro CMS. Both styles are supported by the same client instance initialized by createClient().

FeatureTyped Router API (client.posts / client["posts"])REST Collection API (client.collection("posts"))
Syntaxclient["posts"].find({ depth: 2 })client.collection<Post>("posts").find({ depth: 2 })
Endpoint/api/trpc/{collection}.{method}/api/{collection}
Type SafetyEnd-to-End compile-time safety via generated KyroAppRouterExplicit generic type annotation (collection<T>(slug))
AutocompleteAutocompletes collection names, input filters, and fieldsAutocompletes standard pagination and query parameters
Slug SourceCompile-time static keysDynamic runtime strings (e.g. client.collection(routeSlug))
Codegen RequiredYes (npx kyro-codegen)No (Optional)
Methods Availablefind, findByID, create, update, delete, countfind, findByID, create, update, delete

1. Typed Router API (client["collection-name"])

The Typed Router API leverages the KyroAppRouter type generated by npx kyro-codegen. It provides full end-to-end type safety, autocompleting collection names, input options, and return types directly in your IDE.

typescript
import { createClient } from "@kyro-cms/connect";
import type { KyroAppRouter } from "./types/kyro";

const client = createClient<KyroAppRouter>({
  url: process.env.PUBLIC_KYRO_URL!,
  apiKey: process.env.KYRO_API_KEY!,
});

// Fully typed query: IDE autocompletes "food-menu-category", inputs, and fields
const categories = await client["food-menu-category"].find({
  depth: 5,
  where: { status: { equals: "published" } }
});

// Single document lookup
const category = await client["food-menu-category"].findByID({ id: "cat-123" });

When to use:

  • When you want maximum IDE autocompletion for collection names and input fields.
  • In frontend applications with a build step where generated type definitions match the CMS schema.
  • When working with fixed, known collection names.

2. REST Collection API (client.collection("slug"))

The REST Collection API provides a traditional RESTful interface using string slugs (client.collection(slug)). It directly targets the /api/{slug} REST routes.

TIP

Automatic Type Population: When createClient<KyroAppRouter>() is initialized with your generated router type, client.collection("slug") automatically populates collection slug autocompletion and document return types directly from KyroAppRouter! You do not need to manually pass generic types like collection<FoodCategory>("food-menu-category").

typescript
import { createClient } from "@kyro-cms/connect";
import type { KyroAppRouter } from "./types/kyro";

const client = createClient<KyroAppRouter>({
  url: process.env.PUBLIC_KYRO_URL!,
  apiKey: process.env.KYRO_API_KEY!,
});

// IDE automatically autocompletes "food-menu-category" and populates category return types!
const categories = await client.collection("food-menu-category").find({
  depth: 5,
});
// categories.docs is automatically typed as FoodMenuCategory[]!

// Single document lookup — return type is automatically FoodMenuCategory | null
const category = await client.collection("food-menu-category").findByID("cat-123");

Dynamic Slugs / Untyped Client: If you instantiate createClient() without a router type or pass dynamic runtime strings (e.g. from route params), you can supply an optional generic:

typescript
const slug = getSlugFromUrl(); // e.g. "posts"
const items = await client.collection<Post>(slug).find();

When to use:

  • When you want a RESTful /api/{collection} interface while still retaining 100% autocompletion and return types from KyroAppRouter.
  • When collection slugs are dynamic at runtime (e.g., URL parameters /api/[collection]/[id]).
  • When creating reusable generic data fetching utilities or custom hooks.

Collection Client

client.collection<T>(slug) returns a CollectionClient<T, F> generic over the document type T and filter type F.

ts
interface Post {
  id: string;
  title: string;
  content: string;
}

const posts = await client
  .collection<Post>("posts")
  .find({ draft: true, where: { title: { contains: "hello" } } });
// posts: CollectionFindResult<Post>

Methods

find(params?)

List documents with pagination and filtering.

ts
const result = await client.collection<Post>("posts").find({
  page: 1,
  limit: 10,
  sort: "createdAt_desc",
  where: { status: { equals: "published" } },
  select: "title,slug",
  depth: 2,
  draft: false,
});

Returns CollectionFindResult<T>:

ts
interface CollectionFindResult<T> {
  docs: T[];
  totalDocs: number;
  page: number;
  totalPages: number;
  hasNextPage: boolean;
  hasPrevPage: boolean;
}

findByID(id, params?)

Get a single document by ID.

ts
const post = await client.collection<Post>("posts").findByID("abc123", {
  draft: true,
});

create(data, params?)

Create a new document.

ts
const post = await client.collection<Post>("posts").create({
  title: "New Post",
  content: "Hello world",
});

update(id, data, params?)

Update an existing document.

ts
const post = await client.collection<Post>("posts").update("abc123", {
  title: "Updated Title",
});

delete(id)

Delete a document.

ts
const result = await client.collection<Post>("posts").delete("abc123");
// { message: "Document deleted successfully" }

CollectionFindParams

ts
interface CollectionFindParams {
  draft?: boolean;
  depth?: number;
  sort?: string;
  page?: number;
  limit?: number;
  select?: string;
  where?: Record<string, unknown>;
}

Note on Relationships (depth): By default, kyro-connect only returns relationship IDs. If your collection or global contains upload or relationship fields and you want the SDK to return the actual linked data (populated objects), you must set the depth parameter to 1 or higher.

GraphQL Client

client.gql<TData, TVars>(query) accepts raw strings or TypedDocumentNode objects for full end-to-end type safety with GraphQL Codegen.

ts
import { graphql } from "@/gql"; // from GraphQL Codegen

const query = graphql(`
  query Posts {
    posts {
      docs {
        id
        title
      }
    }
  }
`);

const result = await client.gql(query);
// result is fully typed

With variables:

ts
const result = await client.gql<
  { post: { id: string; title: string } },
  { id: string }
>(`
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
    }
  }
`, { id: "abc123" });

File Upload

client.upload<TResult>(file, config) sends a multipart upload request.

ts
const file = new File(["..."], "photo.jpg", { type: "image/jpeg" });

const result = await client.upload<{ url: string }>(file, {
  collection: "media",
});

Legacy Proxy Access

The old tRPC proxy pattern remains available via client.$proxy or direct bracket access for backwards compatibility.

ts
// Legacy proxy — still works
const posts = await client["posts"].find({ page: 1 });
const post = await client.$proxy.posts.findByID({ id: "abc123" });

Error Handling

All failed requests throw a KyroConnectError with structured properties.

ts
import { createClient, KyroConnectError } from "@kyro-cms/connect";

try {
  await client.collection("posts").find();
} catch (err) {
  if (err instanceof KyroConnectError) {
    console.error(err.code);    // e.g. "BAD_USER_INPUT"
    console.error(err.status);  // HTTP status code
    console.error(err.data);    // Server error payload
  }
}

Type Exports

ts
import {
  ClientOptions,
  CollectionClient,
  CollectionFindResult,
  CollectionFindParams,
  GqlClient,
  UploadClient,
  KyroClient,
  KyroConnectError,
} from "@kyro-cms/connect";

Codegen

kyro-connect ships with two codegen tools for different workflows:

kyro generate:types — Local (build-time)

Reads your Kyro config files from disk and generates TypeScript types. No server needed. Part of the @kyro-cms/core CLI.

bash
kyro generate:types --output ./src/lib/kyro-types.ts

Best for: development, monorepos, and CI where you have access to the config files but not a running server.

kyro-codegen — Remote (runtime)

Fetches the schema from a live Kyro server via HTTP and generates types. Ships as a binary with kyro-connect.

bash
npx kyro-codegen \
  --url https://my-cms.example.com \
  --api-key kc_abc123 \
  --output ./src/kyro.generated.d.ts
FlagRequiredDefaultDescription
--urlYesBase URL of a running Kyro CMS server
--api-keyYesAPI key with access to the schema endpoint
--outputNokyro.generated.d.tsOutput path for the generated .d.ts file

The tool hits {url}/kyro/schema and generates a complete KyroAppRouter interface alongside per-collection document types, input types, and discriminated unions for blocks and enums.

Best for: deployment pipelines where the consumer is separate from the CMS server, or when you want types that exactly match the running server schema.

ts
// Generated output can be used as:
import { createClient } from "@kyro-cms/connect";
import type { KyroAppRouter } from "./kyro.generated.d.ts";

const client = createClient<KyroAppRouter>({
  url: "https://my-cms.example.com",
  apiKey: "kc_abc123",
});

Both tools produce the same format — a .d.ts file with KyroAppRouter and per-collection interfaces that you pass as the generic parameter to createClient<KyroAppRouter>().

Released under the MIT License.