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:
- The Payload instance is created once and reused
- The cms-client package never imports config or Payload itself
- 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/-- seepackages/cms-core/AGENTS.md - CMS app (where getPayload is called):
apps/cms/-- seeapps/cms/AGENTS.md - Core use cases (consumers of this client):
packages/core/-- seepackages/core/AGENTS.md