8.8 KiB
| title | aliases | tags | sources | created | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Payload CMS — Queries Overview |
|
|
2026-05-15 | 2026-05-15 |
PayloadCMS — Queries
Key Takeaways
- One language, three APIs — same
Whereobject works in wiki/payloadcms/local-api, wiki/payloadcms/rest-api, and wiki/payloadcms/graphql-overview; only the wrapper syntax differs - 15 operators cover equality, comparison, text search, list membership, existence, and geospatial queries
and/ornesting can be arbitrarily deep for complex filter logic- Dot notation (
'artists.featured') accesses nested and relationship fields without joins - REST queries use
qs-esm— complexWhereobjects stringify cleanly; avoid hand-crafting query strings alloperator is MongoDB-only — do not use with Postgres/SQLite adapters- Performance stack:
index: true+depth: 0+limit: 1+pagination: false+selectcombined is the fastest pattern for unique-field lookups depthis ignored in GraphQL — shape your GraphQL query fields instead- Add
index: trueto any field queried or sorted frequently — biggest single perf win
Overview
Payload's querying language is uniform across all three APIs (Local, REST, GraphQL). You write the same logic; only the syntax wrapper differs.
- Query operators work on any indexed or non-indexed field
and/orcan be nested arbitrarily deep- Dot notation accesses nested/related fields
- Add
index: trueto frequently queried fields for significant speed gains
Operators
| Operator | Use case |
|---|---|
equals |
Exact match |
not_equals |
Exclude exact value |
greater_than / greater_than_equal |
Numeric or date |
less_than / less_than_equal |
Numeric or date |
like |
Case-insensitive substring; all words must be present |
contains |
Case-insensitive substring (single value) |
in |
Value in comma-delimited list |
not_in |
Value NOT in comma-delimited list |
all |
All values in list present (MongoDB only) |
exists |
true = field present, false = field absent |
near |
Point field: lng,lat,maxMeters,minMeters |
within |
Point field: inside GeoJSON area |
intersects |
Point field: intersects GeoJSON area |
Where Syntax
Local API
import type { Where } from 'payload'
const posts = await payload.find({
collection: 'posts',
where: {
color: { equals: 'blue' },
},
})
REST API
GET /api/posts?where[color][equals]=blue
Use qs-esm for complex queries:
import { stringify } from 'qs-esm'
const query = stringify(
{ where: { color: { equals: 'mint' } } },
{ addQueryPrefix: true },
)
const res = await fetch(`/api/posts${query}`)
GraphQL
query {
Posts(where: { color: { equals: mint } }) {
docs { color }
totalDocs
}
}
And / Or Logic
const query: Where = {
or: [
{ color: { equals: 'mint' } },
{
and: [
{ color: { equals: 'white' } },
{ featured: { equals: false } },
],
},
],
}
Nested / Relational Fields (dot notation)
const query: Where = {
'artists.featured': { exists: true },
}
Depth
Controls how many levels of relationships are auto-populated. Default: 2. Has no effect in GraphQL (use query shape instead).
| depth | Behavior |
|---|---|
0 |
Return only IDs for related docs |
1 |
Populate one level |
2 (default) |
Populate two levels |
// Local API
await payload.find({ collection: 'posts', depth: 1 })
// REST
fetch('/api/posts?depth=1')
Global default depth
export default buildConfig({
defaultDepth: 1,
})
Per-field max depth (overrides request depth)
{
name: 'author',
type: 'relationship',
relationTo: 'users',
maxDepth: 2,
}
Pagination
find queries are paginated by default.
| Control | Default | Description |
|---|---|---|
limit |
10 |
Docs per page; 0 disables pagination |
page |
1 |
Page number |
pagination |
true |
false = return all docs, skip count |
// Local API
await payload.find({ collection: 'posts', limit: 10, page: 2 })
// REST
fetch('/api/posts?limit=10&page=2')
Response shape
{
"docs": [...],
"totalDocs": 6,
"limit": 10,
"totalPages": 1,
"page": 1,
"pagingCounter": 1,
"hasPrevPage": false,
"hasNextPage": false,
"prevPage": null,
"nextPage": null
}
Disable pagination (performance)
await payload.find({
collection: 'posts',
where: { slug: { equals: 'my-post' } },
pagination: false,
limit: 1, // combine with limit for best perf
})
Sort
- Ascending: field name (e.g.
title) - Descending: prefix with
-(e.g.-createdAt) - Multiple fields: array (Local) or comma-separated (REST/GraphQL)
- Only stored (non-virtual) fields can be sorted
// Local API — single field
await payload.find({ collection: 'posts', sort: '-createdAt' })
// Local API — multiple fields
await payload.find({ collection: 'posts', sort: ['priority', '-createdAt'] })
// REST — single
fetch('/api/posts?sort=-createdAt')
// REST — multiple
fetch('/api/posts?sort=priority,-createdAt')
// GraphQL
// Posts(sort: "priority,-createdAt")
Select (Field Projection)
Returns only specified fields. Reduces response size and skips field hooks for unselected fields. id is always returned.
Include mode
await payload.find({
collection: 'posts',
select: {
text: true,
group: { number: true }, // nested field
array: true, // entire array
},
})
Exclude mode
await payload.find({
collection: 'posts',
select: {
array: false,
group: { number: false },
},
})
REST
GET /api/posts?select[color]=true&select[group][number]=true
Or with qs-esm:
const query = stringify({ select: { text: true, group: { number: true } } }, { addQueryPrefix: true })
fetch(`/api/posts${query}`)
Empty select → returns only id
const post = await payload.findByID({ collection: 'posts', id: '1', select: {} })
// { id: '1' }
Populate (override defaultPopulate)
defaultPopulate on a collection sets which fields are returned when that collection is populated as a relationship. Override per-request with populate:
// Collection config
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
defaultPopulate: { slug: true }, // only return slug when populated
fields: [{ name: 'slug', type: 'text' }],
}
// Override at query time (Local API)
await payload.find({
collection: 'posts',
populate: {
pages: { text: true }, // override pages' defaultPopulate
},
})
// REST
fetch('/api/posts?populate[pages][text]=true')
Upload collections with defaultPopulate: always include filename: true alongside url: true, otherwise url returns null.
Entity-level Select Config
Force fields to always be selected regardless of caller's select:
export const Posts: CollectionConfig = {
slug: 'posts',
select: ({ select }) => (select ? { ...select, title: true } : undefined),
fields: [],
}
Return undefined to leave caller's select unchanged. This runs before the read; no per-document data available.
Performance Checklist
index: trueon frequently queried/sorted fieldsdepth: 0when you only need IDslimit: 1+pagination: falsewhen querying by unique field (e.g. slug)selectto return only needed fieldsdefaultPopulateto limit relationship payload size
Gotchas
alloperator only works with MongoDB adapterdepthis ignored by GraphQL — control population via query shapebeforeRead/afterReadhooks may not receive full doc whenselectis active- Sorting on virtual fields is not supported unless linked to a relationship field
limit: 0has the same effect aspagination: false
Related
- wiki/payloadcms/local-api — direct DB access via
payload.find() - wiki/payloadcms/rest-api — HTTP endpoints with query string syntax
- wiki/payloadcms/graphql-overview —
whereargument in queries/mutations - wiki/payloadcms/queries-depth — relationship population control
- wiki/payloadcms/database-indexes —
index: truefield config - wiki/payloadcms/fields-point —
near/within/intersectsoperators - wiki/payloadcms/performance-overview — full perf checklist