201 lines
6.3 KiB
Markdown
201 lines
6.3 KiB
Markdown
---
|
|
tags: [payloadcms, tech-patterns]
|
|
topic: payloadcms
|
|
sources:
|
|
- https://payloadcms.com/docs/rest-api/overview
|
|
created: 2026-05-15
|
|
---
|
|
|
|
# PayloadCMS — REST API
|
|
|
|
## Overview
|
|
|
|
Payload auto-generates a fully functional REST API from Collection and Global configs. All routes are prefixed with `/api` (configurable via `routes.api`).
|
|
|
|
- Collection slugs must be **kebab-case**
|
|
- Supports: depth, locale, fallback-locale, select, populate, limit, page, sort, where, joins
|
|
- Type-safe client available via `@payloadcms/sdk` (beta)
|
|
|
|
## Collection Endpoints
|
|
|
|
| Method | Path | Operation |
|
|
|--------|------|-----------|
|
|
| `GET` | `/api/{slug}` | Find (paginated) |
|
|
| `GET` | `/api/{slug}/{id}` | Find by ID |
|
|
| `GET` | `/api/{slug}/count` | Count documents |
|
|
| `POST` | `/api/{slug}` | Create |
|
|
| `PATCH` | `/api/{slug}` | Update many (with `where`) |
|
|
| `PATCH` | `/api/{slug}/{id}` | Update by ID |
|
|
| `DELETE` | `/api/{slug}` | Delete many (with `where`) |
|
|
| `DELETE` | `/api/{slug}/{id}` | Delete by ID |
|
|
|
|
## Auth Endpoints (auth-enabled collections)
|
|
|
|
| Method | Path | Operation |
|
|
|--------|------|-----------|
|
|
| `POST` | `/api/{slug}/login` | Login — returns JWT + user |
|
|
| `POST` | `/api/{slug}/logout` | Logout |
|
|
| `POST` | `/api/{slug}/refresh-token` | Refresh JWT |
|
|
| `GET` | `/api/{slug}/me` | Current user |
|
|
| `POST` | `/api/{slug}/forgot-password` | Trigger password reset |
|
|
| `POST` | `/api/{slug}/reset-password` | Apply new password with token |
|
|
| `POST` | `/api/{slug}/verify/{token}` | Verify email |
|
|
| `POST` | `/api/{slug}/unlock` | Unlock locked account |
|
|
|
|
## Global Endpoints
|
|
|
|
| Method | Path | Operation |
|
|
|--------|------|-----------|
|
|
| `GET` | `/api/globals/{slug}` | Get global |
|
|
| `POST` | `/api/globals/{slug}` | Update global |
|
|
|
|
## Query Parameters
|
|
|
|
| Param | Type | Description |
|
|
|-------|------|-------------|
|
|
| `depth` | `number` | Relationship population depth (default: `2`) |
|
|
| `locale` | `string` | Return doc in a specific locale |
|
|
| `fallback-locale` | `string` | Fallback locale |
|
|
| `select` | `object` | Field projection (`select[title]=true`) |
|
|
| `populate` | `object` | Override `defaultPopulate` for related docs |
|
|
| `limit` | `number` | Docs per page (default: `10`) |
|
|
| `page` | `number` | Page number |
|
|
| `sort` | `string` | Field name, prefix `-` for descending, comma-separated for multi |
|
|
| `where` | `object` | Filter query (see [[wiki/payloadcms/queries|Queries]]) |
|
|
| `joins` | `object` | Custom request per join field |
|
|
|
|
## Code Examples
|
|
|
|
### Basic fetch
|
|
|
|
```ts
|
|
// Find published posts, sorted newest first
|
|
const res = await fetch('/api/posts?where[status][equals]=published&sort=-createdAt&limit=10')
|
|
const { docs, totalDocs, totalPages } = await res.json()
|
|
```
|
|
|
|
### Complex query with qs-esm
|
|
|
|
```ts
|
|
import { stringify } from 'qs-esm'
|
|
|
|
const query = stringify({
|
|
where: { color: { equals: 'mint' } },
|
|
select: { title: true, slug: true },
|
|
sort: '-createdAt',
|
|
limit: 20,
|
|
}, { addQueryPrefix: true })
|
|
|
|
const res = await fetch(`/api/posts${query}`)
|
|
```
|
|
|
|
### Auth: login and use token
|
|
|
|
```ts
|
|
const { token } = await fetch('/api/users/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: 'dev@example.com', password: 'secret' }),
|
|
}).then(r => r.json())
|
|
|
|
// Authenticated request
|
|
const res = await fetch('/api/posts', {
|
|
headers: { Authorization: `JWT ${token}` },
|
|
})
|
|
```
|
|
|
|
## PayloadCMS SDK (`@payloadcms/sdk`)
|
|
|
|
Type-safe REST client, API similar to Local API:
|
|
|
|
```ts
|
|
import { PayloadSDK } from '@payloadcms/sdk'
|
|
import type { Config } from './payload-types'
|
|
|
|
const sdk = new PayloadSDK<Config>({ baseURL: 'https://example.com/api' })
|
|
|
|
// Operations
|
|
await sdk.find({ collection: 'posts', limit: 10, where: { _status: { equals: 'published' } } })
|
|
await sdk.findByID({ collection: 'posts', id })
|
|
await sdk.create({ collection: 'posts', data: { text: 'hello' } })
|
|
await sdk.update({ collection: 'posts', id, data: { text: 'updated' } })
|
|
await sdk.delete({ collection: 'posts', id })
|
|
await sdk.findGlobal({ slug: 'header' })
|
|
await sdk.updateGlobal({ slug: 'header', data: { text: 'new' } })
|
|
|
|
// Auth operations
|
|
await sdk.login({ collection: 'users', data: { email, password } })
|
|
await sdk.me({ collection: 'users' }, { headers: { Authorization: `JWT ${token}` } })
|
|
await sdk.refreshToken({ collection: 'users' }, { headers: { Authorization: `JWT ${token}` } })
|
|
```
|
|
|
|
### Custom endpoint
|
|
|
|
```ts
|
|
await sdk.request({ method: 'POST', path: '/send-data', json: { id: 1 } })
|
|
```
|
|
|
|
## Custom Endpoints
|
|
|
|
Add `endpoints` array to collection, global, or root config:
|
|
|
|
```ts
|
|
export const Orders: CollectionConfig = {
|
|
slug: 'orders',
|
|
endpoints: [
|
|
{
|
|
path: '/:id/tracking',
|
|
method: 'get',
|
|
handler: async (req) => {
|
|
const tracking = await getTrackingInfo(req.routeParams.id)
|
|
if (!tracking) return Response.json({ error: 'not found' }, { status: 404 })
|
|
return Response.json(tracking)
|
|
},
|
|
},
|
|
],
|
|
}
|
|
```
|
|
|
|
### Custom endpoint helpers
|
|
|
|
```ts
|
|
import { addDataAndFileToRequest, addLocalesToRequestFromData, headersWithCors } from 'payload'
|
|
|
|
// Parse body + file
|
|
await addDataAndFileToRequest(req) // populates req.data and req.file
|
|
|
|
// Parse locale params
|
|
await addLocalesToRequestFromData(req) // populates req.locale and req.fallbackLocale
|
|
|
|
// CORS headers
|
|
return Response.json(data, { headers: headersWithCors({ headers: new Headers(), req }) })
|
|
```
|
|
|
|
## Method Override (long GET queries)
|
|
|
|
When query strings are too long, use POST with `X-Payload-HTTP-Method-Override: GET`:
|
|
|
|
```ts
|
|
const res = await fetch('/api/posts', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-Payload-HTTP-Method-Override': 'GET',
|
|
},
|
|
body: qs.stringify({ depth: 1, locale: 'en' }),
|
|
})
|
|
```
|
|
|
|
## Gotchas
|
|
|
|
- Custom endpoints are **not authenticated by default** — add `if (!req.user)` checks manually
|
|
- `req.data` is not auto-populated in custom endpoints — call `await addDataAndFileToRequest(req)` first
|
|
- `req.locale` / `req.fallbackLocale` are not populated in custom endpoints — use `addLocalesToRequestFromData`
|
|
- Custom endpoints with `root: true` bypass `routes.api` — only valid on top-level Payload config, not on collections/globals
|
|
- SDK is in **beta** — may have breaking changes in minor versions
|
|
- `limit: 0` automatically disables pagination
|
|
|
|
## Related
|
|
|
|
- [[wiki/payloadcms/queries|Queries]]
|
|
- [[wiki/payloadcms/local-api|Local API]]
|