Skip to content

Filtering & Querying

Every list endpoint generated by Resource(...) accepts a rich set of query parameters for filtering, sorting, field selection, and searching. You write no custom handler code — registering the resource is enough.

There are two filter syntaxes, and they compose: suffix operators on query parameters for simple cases, and a JSON where clause for complex boolean logic.

Exact match

The simplest filter is a query parameter whose name matches a model field:

GET /products?in_stock=true
GET /products?sku=MUG-001
GET /orders?status=paid

Values are type-coerced before being sent to MongoDB:

  • true and false become booleans.
  • null and none become None.
  • Integer-looking strings become integers.
  • Float-looking strings become floats.
  • 24-character hex strings become ObjectIds (so ?tenant_id=507f... works as expected).
  • Everything else stays a string.

Suffix operators

Use double-underscore operators for comparisons, ranges, and set membership:

Operator Example MongoDB equivalent
gt ?age__gt=18 {"age": {"$gt": 18}}
gte ?price__gte=100 {"price": {"$gte": 100}}
lt ?age__lt=65 {"age": {"$lt": 65}}
lte ?price__lte=999 {"price": {"$lte": 999}}
ne ?status__ne=cancelled {"status": {"$ne": "cancelled"}}
in ?status__in=paid,pending {"status": {"$in": ["paid", "pending"]}}
nin ?status__nin=cancelled,expired {"status": {"$nin": [...]}}
exists ?deleted_at__exists=false {"deleted_at": {"$exists": false}}
regex ?name__regex=^Ali {"name": {"$regex": "^Ali", "$options": "i"}}

in and nin take a comma-separated list. exists takes true or false. regex is always case-insensitive.

curl "https://api.example.com/products?price__gte=100&price__lt=500&in_stock=true"
curl "https://api.example.com/users?status__in=active,trial&created_at__gte=2026-01-01"

The where clause

For conditions that cannot be expressed with flat query parameters — $or branches, nested objects, negation — pass a JSON object in the where query parameter:

curl -G https://api.example.com/products \
  --data-urlencode 'where={"status":"active","price":{"$gt":100}}'

curl -G https://api.example.com/orders \
  --data-urlencode 'where={"$or":[{"status":"paid"},{"status":"partial"}]}'

curl -G https://api.example.com/users \
  --data-urlencode 'where={"email":{"$regex":"^admin@","$options":"i"}}'

The where parameter accepts a strict subset of MongoDB operators. Every operator that appears in the parsed object — at any nesting level — is checked against an allow-list, and unknown operators are rejected with a 400.

Allowed operators:

$gt   $gte  $lt   $lte  $ne
$in   $nin  $exists  $regex  $options
$and  $or   $not

Operators that execute JavaScript or expose server state — $where, $expr, $function, $accumulator — are never allowed, even if a client tries to embed them. Invalid JSON or a non-object top-level value also returns a 400.

Combining where with query parameters

The where clause and suffix-operator parameters merge. Query parameters override matching keys in where if both specify the same field:

GET /orders?where={"customer_id":"507f..."}&status__in=paid,partial&sort=-created_at

Sorting

sort accepts one or more comma-separated field names. Prefix a field with - for descending order (leading + is also allowed for explicit ascending).

GET /products?sort=name
GET /orders?sort=-created_at
GET /products?sort=-in_stock,name

Without a sort parameter, Craft Easy uses the resource's default_sort (typically -created_at).

Field projection

Use fields to return only selected fields — useful for list views that need fewer columns:

GET /products?fields=name,sku,price

_id is always included in the response even if not listed. Field projection applies to both offset and cursor pagination modes.

When a resource declares search_fields, the q parameter performs case-insensitive prefix matching across those fields. Each whitespace-separated word in the query must match at least one field — multi-word queries AND the matches together:

app.register_resource(Resource(
    User,
    path="/users",
    search_fields=["name", "email", "company"],
))
GET /users?q=alice
GET /users?q=alice acme    # matches records where every word hits any search field

Calling ?q=... on a resource without search_fields returns 400 — Search is not enabled for this resource.

Tag filters

Models with a tags field pick up three extra filter parameters automatically:

Parameter Semantics
tags__in Any tag matches (comma-separated).
tags__all Every listed tag must be present.
tags__nin None of the listed tags may be present.
GET /products?tags__all=featured,seasonal
GET /articles?tags__in=news,update

Reserved parameters

The following query parameters are never treated as filters — they control pagination, sorting, projection, and response shape:

page  per_page  cursor  limit  direction
sort  fields  where  q
include_deleted  populate
tags__in  tags__all  tags__nin

Any other parameter is either a field match or a suffix-operator filter.

Example: combining everything

curl -G https://api.example.com/orders \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'where={"$or":[{"status":"paid"},{"status":"partial"}]}' \
  --data-urlencode 'amount__gte=100' \
  --data-urlencode 'created_at__gte=2026-01-01' \
  --data-urlencode 'customer_name__regex=^Acme' \
  --data-urlencode 'sort=-created_at' \
  --data-urlencode 'fields=id,reference,amount,status,created_at' \
  --data-urlencode 'per_page=50'

Source

  • craft_easy/core/crud/filtering.pybuild_filters, parse_where, build_sort, build_search_filter, operator allow-list, and the reserved-parameter set.