268 lines
8.3 KiB
Markdown
268 lines
8.3 KiB
Markdown
---
|
|
title: "PayloadCMS — Local API"
|
|
aliases: [local-api, payload-local-api, payload-server-api]
|
|
tags: [payloadcms, local-api, server-side, nextjs, typescript]
|
|
sources:
|
|
- raw/local-api__overview.md
|
|
- https://payloadcms.com/docs/local-api/overview
|
|
- https://payloadcms.com/docs/local-api/outside-nextjs
|
|
- https://payloadcms.com/docs/local-api/server-functions
|
|
- https://payloadcms.com/docs/local-api/access-control
|
|
created: 2026-05-15
|
|
updated: 2026-05-15
|
|
---
|
|
|
|
# PayloadCMS — Local API
|
|
|
|
## Overview
|
|
|
|
The Local API executes the same operations as REST/GraphQL but **directly against the database** — no HTTP layer, no latency. Ideal for React Server Components, seed scripts, hooks, and custom route handlers.
|
|
|
|
- Access via `req.payload` (in hooks/access control) or import `getPayload`
|
|
- **Access control is skipped by default** (`overrideAccess: true`)
|
|
- Full TypeScript inference from generated types
|
|
- Works in Next.js (with HMR) and standalone scripts
|
|
|
|
## Accessing Payload
|
|
|
|
```ts
|
|
// Option 1: from hook/access control args
|
|
const afterChangeHook: CollectionAfterChangeHook = async ({ req: { payload } }) => {
|
|
const posts = await payload.find({ collection: 'posts' })
|
|
}
|
|
|
|
// Option 2: import and initialize
|
|
import { getPayload } from 'payload'
|
|
import config from '@payload-config'
|
|
|
|
const payload = await getPayload({ config })
|
|
```
|
|
|
|
## Collection Operations
|
|
|
|
### Create
|
|
|
|
```ts
|
|
const post = await payload.create({
|
|
collection: 'posts',
|
|
data: { title: 'sure', description: 'maybe' },
|
|
locale: 'en',
|
|
overrideAccess: true,
|
|
filePath: path.resolve(__dirname, './image.jpg'), // for upload collections
|
|
duplicateFromID: 'existing-doc-id', // optional clone
|
|
})
|
|
```
|
|
|
|
### Find
|
|
|
|
```ts
|
|
const result = await payload.find({
|
|
collection: 'posts',
|
|
depth: 2,
|
|
page: 1,
|
|
limit: 10,
|
|
pagination: false, // skip count queries
|
|
where: { status: { equals: 'published' } },
|
|
sort: '-createdAt',
|
|
locale: 'en',
|
|
overrideAccess: false,
|
|
user: dummyUser,
|
|
})
|
|
```
|
|
|
|
### Find by ID
|
|
|
|
```ts
|
|
const result = await payload.findByID({
|
|
collection: 'posts',
|
|
id: '507f1f77bcf86cd799439011',
|
|
depth: 2,
|
|
disableErrors: true, // returns null instead of throwing
|
|
})
|
|
```
|
|
|
|
### Count / FindDistinct
|
|
|
|
```ts
|
|
const { totalDocs } = await payload.count({ collection: 'posts', where: {} })
|
|
|
|
const { values } = await payload.findDistinct({
|
|
collection: 'posts',
|
|
field: 'title',
|
|
sort: 'title',
|
|
})
|
|
```
|
|
|
|
### Update (by ID or bulk)
|
|
|
|
```ts
|
|
// By ID
|
|
await payload.update({ collection: 'posts', id: '...', data: { title: 'new' } })
|
|
|
|
// Bulk — returns { docs: [], errors: [] }
|
|
await payload.update({
|
|
collection: 'posts',
|
|
where: { fieldName: { equals: 'value' } },
|
|
data: { title: 'updated' },
|
|
overrideLock: false, // enforce document locks
|
|
})
|
|
```
|
|
|
|
### Delete (by ID or bulk)
|
|
|
|
```ts
|
|
await payload.delete({ collection: 'posts', id: '...' })
|
|
await payload.delete({ collection: 'posts', where: { fieldName: { equals: 'v' } } })
|
|
```
|
|
|
|
## Auth Operations
|
|
|
|
```ts
|
|
// Login
|
|
const { token, user, exp } = await payload.login({
|
|
collection: 'users',
|
|
data: { email: 'dev@payload.com', password: 'secret' },
|
|
})
|
|
|
|
// Current user from headers
|
|
const { user, permissions } = await payload.auth({ headers, canSetHeaders: false })
|
|
|
|
// Password reset flow
|
|
const token = await payload.forgotPassword({ collection: 'users', data: { email: '...' } })
|
|
const result = await payload.resetPassword({ collection: 'users', data: { password: 'new', token } })
|
|
|
|
// Verify email
|
|
await payload.verifyEmail({ collection: 'users', token: '...' })
|
|
await payload.unlock({ collection: 'users', data: { email: '...' } })
|
|
```
|
|
|
|
## Global Operations
|
|
|
|
```ts
|
|
const header = await payload.findGlobal({ slug: 'header', depth: 2 })
|
|
|
|
await payload.updateGlobal({
|
|
slug: 'header',
|
|
data: { nav: [{ url: 'https://example.com' }] },
|
|
overrideLock: false,
|
|
})
|
|
```
|
|
|
|
## Server Functions (Next.js)
|
|
|
|
Local API cannot be called from client components directly. Use `'use server'` functions as a bridge:
|
|
|
|
```ts
|
|
'use server'
|
|
import { getPayload } from 'payload'
|
|
import config from '@payload-config'
|
|
|
|
export async function createPost(data) {
|
|
const payload = await getPayload({ config })
|
|
return await payload.create({ collection: 'posts', data })
|
|
}
|
|
```
|
|
|
|
### Built-in auth server functions (`@payloadcms/next/auth`)
|
|
|
|
```ts
|
|
import { login, logout, refresh } from '@payloadcms/next/auth'
|
|
|
|
// login — sets auth cookie
|
|
await login({ collection: 'users', config, email, password })
|
|
|
|
// logout — clears auth cookie
|
|
await logout({ allSessions: true, config })
|
|
|
|
// refresh — rotates JWT
|
|
await refresh({ config })
|
|
```
|
|
|
|
## Outside Next.js (Standalone Scripts)
|
|
|
|
```ts
|
|
import { getPayload } from 'payload'
|
|
import config from '@payload-config'
|
|
|
|
const seed = async () => {
|
|
const payload = await getPayload({ config })
|
|
await payload.create({ collection: 'users', data: { email: 'dev@example.com', password: 'pw' } })
|
|
}
|
|
await seed()
|
|
```
|
|
|
|
Run with: `payload run src/seed.ts`
|
|
- Loads `.env` the same way Next.js does (no `dotenv` needed)
|
|
- Flags: `--use-swc` (faster, needs `@swc-node/register`), `--disable-transpile` (for Bun)
|
|
|
|
## Key Options Reference
|
|
|
|
| Option | Default | Description |
|
|
|--------|---------|-------------|
|
|
| `overrideAccess` | `true` | Skip access control checks |
|
|
| `user` | — | User to check against (when `overrideAccess: false`) |
|
|
| `depth` | `2` | Relationship auto-population depth |
|
|
| `locale` / `fallbackLocale` | — | Localization |
|
|
| `select` | — | Field projection |
|
|
| `populate` | — | Override `defaultPopulate` for related docs |
|
|
| `pagination` | `true` | Set `false` to skip count queries |
|
|
| `showHiddenFields` | `false` | Return hidden fields |
|
|
| `overrideLock` | `true` | Ignore document locks |
|
|
| `disableErrors` | `false` | Return null/empty instead of throwing |
|
|
| `disableTransaction` | `false` | Skip DB transaction |
|
|
| `context` | — | Pass extra data to hooks via `req.context` |
|
|
|
|
## Access Control
|
|
|
|
```ts
|
|
// Skip access control (default)
|
|
await payload.create({ collection: 'users', data: { ... } })
|
|
|
|
// Enforce access control
|
|
await payload.create({
|
|
collection: 'users',
|
|
overrideAccess: false,
|
|
user, // authenticated user document
|
|
data: { ... },
|
|
})
|
|
```
|
|
|
|
## Transactions
|
|
|
|
Pass `req` through all local operations when using Postgres or MongoDB replica sets:
|
|
|
|
```ts
|
|
const post = await payload.find({ collection: 'posts', req })
|
|
```
|
|
|
|
## Gotchas
|
|
|
|
- Access control is **off by default** — easy to accidentally expose privileged data in seed scripts
|
|
- `disableErrors: true` silences `findByID` errors; use carefully in production
|
|
- `payload run` handles `.env` loading; avoid `dotenv` alongside it
|
|
- Server functions must use `'use server'` directive and can't directly import Payload config in client components
|
|
- For file uploads in server functions, merge the `File` object into `data` before calling `payload.create()`
|
|
- `beforeRead`/`afterRead` hooks may not receive full doc when `select` is active — use entity-level `select` config to force fields
|
|
|
|
## Key Takeaways
|
|
|
|
- **No HTTP overhead** — Local API hits the DB directly; ideal for RSC, seeds, hooks, custom route handlers
|
|
- **Access control off by default** (`overrideAccess: true`) — always set `overrideAccess: false` + `user` for user-facing logic
|
|
- **Two access patterns:** `req.payload` (in hooks/access control) vs `await getPayload({ config })` (everywhere else)
|
|
- **Thread `req` through** all operations when using Postgres or MongoDB replica sets (transactions)
|
|
- **Full TypeScript inference** — `payload.create({ collection: 'posts', data: { ... } })` returns typed `Post`
|
|
- **`pagination: false`** skips count queries — useful for internal data fetching where counts aren't needed
|
|
- **`disableErrors: true`** makes `findByID` return `null` instead of throwing — use cautiously in production
|
|
- **`context`** passes extra metadata to hooks without polluting document data (e.g. `triggerBeforeChange` flag)
|
|
- **Server functions** (`'use server'`) are the bridge for using Local API from client-triggered actions in Next.js
|
|
- **`payload run`** CLI handles `.env` automatically for standalone scripts — no need for `dotenv`
|
|
|
|
## Related
|
|
|
|
- [[wiki/payloadcms/queries|Queries]]
|
|
- [[wiki/payloadcms/rest-api|REST API]]
|
|
- [[wiki/payloadcms/hooks|Hooks]]
|
|
- [[wiki/payloadcms/authentication-operations|Authentication — Operations]]
|
|
- [[wiki/payloadcms/local-api-outside-nextjs|Local API — Outside Next.js]]
|
|
- [[wiki/payloadcms/local-api-access-control|Local API — Access Control]]
|
|
- [[wiki/payloadcms/database-transactions|Database — Transactions]]
|