obsidian/wiki/payloadcms/local-api.md
2026-05-15 16:14:29 +01:00

8.3 KiB

title aliases tags sources created updated
PayloadCMS — Local API
local-api
payload-local-api
payload-server-api
payloadcms
local-api
server-side
nextjs
typescript
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
2026-05-15 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

// 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

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

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

const result = await payload.findByID({
  collection: 'posts',
  id: '507f1f77bcf86cd799439011',
  depth: 2,
  disableErrors: true, // returns null instead of throwing
})

Count / FindDistinct

const { totalDocs } = await payload.count({ collection: 'posts', where: {} })

const { values } = await payload.findDistinct({
  collection: 'posts',
  field: 'title',
  sort: 'title',
})

Update (by ID or bulk)

// 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)

await payload.delete({ collection: 'posts', id: '...' })
await payload.delete({ collection: 'posts', where: { fieldName: { equals: 'v' } } })

Auth Operations

// 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

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:

'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)

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)

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

// 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:

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 inferencepayload.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