Files
agentic-dev-template/packages/cms-client/AGENTS.md

7.2 KiB

@repo/cms-client -- Dual-Mode Payload Client

Purpose

Provides a typed, uniform interface for accessing Payload CMS data via either the Local API (direct in-process calls) or the HTTP REST API (network requests). The consuming app chooses the mode at startup and injects the Payload instance -- this package never imports it.

NEVER import from @repo/cms-core, @repo/core, or any apps/* package.

This package is completely standalone. The Payload instance is INJECTED at app startup, never imported by this package.

File Structure

packages/cms-client/
  src/
    types.ts          # FindOptions, PayloadClientResult, PayloadClient interface
    client.ts         # createPayloadClient() factory -- returns Local or HTTP client
    local-client.ts   # LocalPayloadClient class -- wraps Payload Local API
    http-client.ts    # HTTPPayloadClient class -- wraps Payload REST API via fetch
    index.ts          # Package entry: re-exports factory, classes, and types
  package.json
  AGENTS.md

Dual-Mode Initialization

Local Mode (primary -- used for server-side code with direct DB access)

Local mode wraps the Payload Local API. It requires a live Payload instance, which is obtained via getPayload() in the consuming app:

// Example: apps/web-next/src/lib/payload.ts
import { getPayload } from "payload";
import config from "@repo/cms-core/src/payload.config";
import { createPayloadClient } from "@repo/cms-client";

const payload = await getPayload({ config });
const client = createPayloadClient({ mode: "local", payload });

// Now use client.find(), client.create(), etc.
const articles = await client.find("articles", {
  where: { status: { equals: "published" } },
  sort: "-publishedAt",
  limit: 10,
});

HTTP Mode (fallback -- used for external services without direct DB access)

HTTP mode makes REST calls to the Payload API. It only needs the base URL:

import { createPayloadClient } from "@repo/cms-client";

const client = createPayloadClient({
  mode: "http",
  baseURL: "http://localhost:3001",
});

// Same API surface as local mode
const articles = await client.find("articles", {
  where: { status: { equals: "published" } },
  limit: 10,
});

Initialization Table

App / Context Mode How initialized Why this mode
apps/cms (server-side) Local getPayload({ config }) from @repo/cms-core Same process as Payload, direct DB access
apps/web-next (server components/actions) Local getPayload({ config }) from @repo/cms-core Server-side rendering needs fast DB access
apps/web-tanstack (server loaders) Local getPayload({ config }) from @repo/cms-core Server-side data loading needs fast DB access
Client-side (browser) N/A Does not use this package directly Browser goes through tRPC, server handles CMS access
External services / microservices HTTP createPayloadClient({ mode: "http", baseURL }) No access to Payload instance, only REST API

PayloadClient API Reference

All methods are available on both Local and HTTP clients via the PayloadClient interface.

find<T>(collection, options?): Promise<PayloadClientResult<T>>

Paginated query for documents in a collection.

const result = await client.find<Article>("articles", {
  where: { status: { equals: "published" } },
  sort: "-publishedAt",
  limit: 10,
  page: 1,
  depth: 2,
  locale: "en",
});
// result.docs, result.totalDocs, result.totalPages, etc.

findByID<T>(collection, id, options?): Promise<T>

Fetch a single document by ID.

const article = await client.findByID<Article>("articles", "abc123", {
  depth: 2,
});

create<T>(collection, data, options?): Promise<T>

Create a new document.

const newArticle = await client.create<Article>("articles", {
  title: "My Article",
  content: "...",
  author: "user-id-123",
  status: "draft",
}, { depth: 1 });

update<T>(collection, id, data, options?): Promise<T>

Update an existing document (partial update).

const updated = await client.update<Article>("articles", "abc123", {
  status: "published",
  publishedAt: new Date().toISOString(),
});

delete<T>(collection, id): Promise<T>

Delete a document by ID.

const deleted = await client.delete<Article>("articles", "abc123");

FindOptions Interface

interface FindOptions {
  where?: Record<string, unknown>;   // Payload query operators ({ field: { equals: value } })
  sort?: string;                     // Field name, prefix with "-" for descending
  limit?: number;                    // Max documents per page (default: 10)
  page?: number;                     // Page number (1-based)
  depth?: number;                    // Relationship population depth (default: 1)
  locale?: string;                   // Locale for localized fields
}

PayloadClientResult Interface

interface PayloadClientResult<T> {
  docs: T[];                  // Array of documents for current page
  totalDocs: number;          // Total matching documents across all pages
  limit: number;              // Max docs per page (as requested)
  totalPages: number;         // Total number of pages
  page: number;               // Current page number (1-based)
  pagingCounter: number;      // Index of first doc on current page
  hasPrevPage: boolean;       // Whether a previous page exists
  hasNextPage: boolean;       // Whether a next page exists
  prevPage: number | null;    // Previous page number, or null
  nextPage: number | null;    // Next page number, or null
}

App Startup Pattern

The Payload instance is always created in the consuming app, then injected into the client:

// apps/web-next/src/lib/payload.ts
import { getPayload } from "payload";
import config from "@repo/cms-core/src/payload.config";
import { createPayloadClient, type PayloadClient } from "@repo/cms-client";

let cachedClient: PayloadClient | null = null;

export async function getPayloadClient(): Promise<PayloadClient> {
  if (cachedClient) return cachedClient;

  const payload = await getPayload({ config });
  cachedClient = createPayloadClient({ mode: "local", payload });
  return cachedClient;
}

This pattern ensures:

  1. The Payload instance is created once and reused
  2. The cms-client package never imports config or Payload itself
  3. Each app controls its own initialization

Type Generation

Payload generates TypeScript types from your collection/global definitions:

cd apps/cms && pnpm generate:types
# Runs: payload generate:types
# Outputs to: packages/cms-core/src/payload-types.ts (configured in payload.config.ts)

After adding or modifying collections/globals in @repo/cms-core, re-run type generation to keep types in sync.

Dependencies

Dependency Purpose
payload Payload type for the Local API client constructor (type-only at build time)

Cross-References

  • CMS configuration: packages/cms-core/ -- see packages/cms-core/AGENTS.md
  • CMS app (where getPayload is called): apps/cms/ -- see apps/cms/AGENTS.md
  • Core use cases (consumers of this client): packages/core/ -- see packages/core/AGENTS.md