# @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: ```typescript // 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: ```typescript 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(collection, options?): Promise>` Paginated query for documents in a collection. ```typescript const result = await client.find
("articles", { where: { status: { equals: "published" } }, sort: "-publishedAt", limit: 10, page: 1, depth: 2, locale: "en", }); // result.docs, result.totalDocs, result.totalPages, etc. ``` ### `findByID(collection, id, options?): Promise` Fetch a single document by ID. ```typescript const article = await client.findByID
("articles", "abc123", { depth: 2, }); ``` ### `create(collection, data, options?): Promise` Create a new document. ```typescript const newArticle = await client.create
("articles", { title: "My Article", content: "...", author: "user-id-123", status: "draft", }, { depth: 1 }); ``` ### `update(collection, id, data, options?): Promise` Update an existing document (partial update). ```typescript const updated = await client.update
("articles", "abc123", { status: "published", publishedAt: new Date().toISOString(), }); ``` ### `delete(collection, id): Promise` Delete a document by ID. ```typescript const deleted = await client.delete
("articles", "abc123"); ``` ## FindOptions Interface ```typescript interface FindOptions { where?: Record; // 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 ```typescript interface PayloadClientResult { 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: ```typescript // 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 { 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: ```bash 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`