API Reference

Query and mutate content with the type-safe tRPC API.

Overview

Schemaful uses tRPC for end-to-end type safety. Your IDE knows the exact shape of every response—no codegen required.

Client Setup
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@schemaful/trpc";

const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "https://your-cms.com/api/trpc",
    }),
  ],
});

Entries

CRUD operations for content entries.

List Entries

const { entries, total } = await trpc.cms.entries.list.query({
  schema: "blog-post",
  locale: "en",
  limit: 10,
  offset: 0,
  orderBy: "createdAt",
  orderDir: "desc",
});

Get Entry

const entry = await trpc.cms.entries.get.query({
  id: "abc123",
  locale: "en",
});

// Access typed content
entry.data.title;  // string
entry.data.author; // EntryLink

Get Entry with Resolved Links

const entry = await trpc.cms.entries.resolve.query({
  id: "abc123",
  locale: "en",
  depth: 2,  // Resolve nested links up to 2 levels
});

// Links are replaced with full entry/asset data
entry.data.author.data.name;  // Resolved author entry

Create Entry

const entry = await trpc.cms.entries.create.mutate({
  schemaId: "blog-post",
  locale: "en",
  data: {
    title: "My First Post",
    slug: "my-first-post",
    content: { nodeType: "document", content: [] },
  },
  publish: true,  // Publish immediately
});

Update Entry

const entry = await trpc.cms.entries.update.mutate({
  id: "abc123",
  locale: "en",
  data: {
    title: "Updated Title",
  },
  publish: true,
});

Delete Entry

await trpc.cms.entries.delete.mutate({
  id: "abc123",
  allLocales: true,  // Delete all locale versions
});

Schemas

Manage content type definitions.

// List all schemas
const schemas = await trpc.cms.schemas.list.query();

// Get schema with fields
const schema = await trpc.cms.schemas.getWithFields.query({
  id: "blog-post",
});

// Create schema (admin only)
await trpc.cms.schemas.create.mutate({
  id: "blog-post",
  name: "Blog Post",
  description: "Articles for the blog",
});

Fields

Define and manage schema fields.

// Add field to schema
await trpc.cms.fields.create.mutate({
  schemaId: "blog-post",
  fieldId: "title",
  name: "Title",
  type: "Symbol",
  required: true,
  localized: true,
  validations: [
    { type: "size", min: 1, max: 100 }
  ],
});

// List fields for schema
const fields = await trpc.cms.fields.listBySchema.query({
  schemaId: "blog-post",
});

Assets

Manage media files and metadata.

// List assets
const { assets, total } = await trpc.cms.assets.list.query({
  locale: "en",
  contentType: "image",  // Filter by type
  limit: 20,
});

// Get asset
const asset = await trpc.cms.assets.get.query({
  id: "img-001",
  locale: "en",
});

// Create asset metadata
await trpc.cms.assets.create.mutate({
  locale: "en",
  title: "Hero Image",
  fileName: "hero.jpg",
  contentType: "image/jpeg",
  url: "https://cdn.example.com/hero.jpg",
  width: 1920,
  height: 1080,
});

Locales

Manage available languages.

// List locales
const locales = await trpc.cms.locales.list.query();

// Get default locale
const defaultLocale = await trpc.cms.locales.getDefault.query();

// Create locale (admin only)
await trpc.cms.locales.create.mutate({
  code: "fr",
  name: "French",
  fallbackCode: "en",
});

Authentication

API endpoints require authentication for mutations. Pass the session token in headers:

const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "https://your-cms.com/api/trpc",
      headers: () => ({
        authorization: `Bearer ${sessionToken}`,
      }),
    }),
  ],
});

Roles:

  • Admin — Full access (schemas, fields, settings)
  • Editor — Create/edit entries and assets
  • Viewer — Read-only access

Error Handling

tRPC throws typed errors you can catch:

import { TRPCClientError } from "@trpc/client";

try {
  await trpc.cms.entries.create.mutate({ ... });
} catch (error) {
  if (error instanceof TRPCClientError) {
    console.log(error.message);     // Error message
    console.log(error.data?.code);  // "UNAUTHORIZED", "NOT_FOUND", etc.
  }
}