16.1 Overview
Every ADL list endpoint (GET /api/v1/{domain}/{resources}) automatically supports a standard set of query parameters for pagination, sorting, filtering, search, relationship loading, scope selection, field selection, distinct values, and counting. No configuration is required — these capabilities are generated from the resource’s field and relationship definitions.
The URL syntax and response format for each parameter are specified in §4.7. This chapter covers the domain author’s perspective: what is automatic, what requires configuration, how parameters interact with other features, and what performance implications to consider.
Automatic vs Configured
| Parameter | Automatic | Configuration Required |
|---|---|---|
Pagination (page, limit, offset) | ✅ | None |
Sorting (sort) | ✅ | None — all stored fields are sortable |
Filtering (filter[field]) | ✅ | None — all stored fields are filterable |
Search (search) | ❌ | features.search: true |
Include (include) | ✅ | None — all declared relationships are includable |
Scopes (scope) | Partial | Auto-generated for soft delete; custom scopes require declaration |
Field selection (fields) | ✅ | None |
| Distinct | ✅ | None |
| Count | ✅ | None |
| Bulk operations | ✅ | None |
16.2 Pagination
All list endpoints are paginated. The default page size is 100 records. The maximum page size is 1000.
Page-Based
GET /api/v1/blog/posts?page=2&limit=25
Returns totalCount in the response, enabling page number controls:
{
"data": {
"items": [ ... ],
"count": 25,
"totalCount": 142
}
}
Offset-Based
GET /api/v1/blog/posts?offset=50&limit=25
Does NOT return totalCount (avoiding the cost of a COUNT query). Use for infinite scroll and “load more” patterns.
Default Sort
When no sort parameter is specified, records are ordered by createdAt ascending. This ensures stable pagination — records do not shift between pages as new records are created.
16.3 Sorting
Any stored field (regular or materialized computed) can be used as a sort key. Virtual computed fields cannot be sorted because they have no database column.
GET /api/v1/orders/orders?sort=-orderTotal,createdAt
Multi-Field Sort
Comma-separated field names apply in order. The first field is the primary sort; subsequent fields break ties:
GET /api/v1/blog/posts?sort=status,-publishedAt
Sort Direction
| Syntax | Direction |
|---|---|
sort=createdAt | Ascending |
sort=-createdAt | Descending |
sort=-price,name | Price descending, then name ascending |
Unsortable Fields
| Field Category | Sortable? | Reason |
|---|---|---|
| Regular stored fields | ✅ | Database column |
| Materialized computed | ✅ | Database column |
| Aggregation computed | ✅ | Database column |
| Virtual computed | ❌ | No database column |
| JSON/component/repeater | ❌ | Cannot ORDER BY JSON |
| Relationship fields | ❌ | Not a column on this table |
Sorting by an unsortable field is silently ignored.
16.4 Filtering
Any stored field can be filtered. Filters are combined with AND.
Filter Operators
| Operator | Syntax | SQL | Description |
|---|---|---|---|
| Equals | filter[status]=published | = 'published' | Exact match (default) |
| Equals | filter[status][eq]=published | = 'published' | Explicit equals |
| Not equals | filter[status][ne]=draft | != 'draft' | Exclude a value |
| Greater than | filter[price][gt]=100 | > 100 | Numeric/date comparison |
| Greater or equal | filter[createdAt][gte]=2026-01-01 | >= '2026-01-01' | Inclusive lower bound |
| Less than | filter[price][lt]=50 | < 50 | Numeric/date comparison |
| Less or equal | filter[age][lte]=65 | <= 65 | Inclusive upper bound |
| Pattern match | filter[title][like]=%deploy% | LIKE '%deploy%' | SQL LIKE with wildcards |
| In list | filter[status][in]=draft,review | IN ('draft','review') | Match any value |
Filtering and Permissions
Field-level read permissions affect filtering. If a user does not have read access to a field, filtering by that field is silently ignored. This prevents information leakage through filter-based probing (e.g., discovering salary ranges by filtering on a field the user cannot read).
Filtering on Non-Existent Fields
Filtering on a field that does not exist is silently ignored. This enables forward-compatible clients that may send parameters the server has not yet defined.
16.5 Search
Full-text search requires features.search: true on the resource (§15.5).
GET /api/v1/blog/posts?search=kubernetes
The search index covers all fields of type string, text, and markdown. The term is matched across all indexed fields simultaneously. Search can be combined with filters, sorting, and pagination.
If features.search is not enabled and ?search= is included, the parameter is silently ignored.
16.6 Relationship Loading
The ?include= parameter eager-loads related records. All declared relationships are includable without configuration.
GET /api/v1/blog/posts/post-1?include=author,tags,comments
List vs Get
| Endpoint | Behavior |
|---|---|
GET /:id | Included relationships populated on the single record. No performance concern. |
GET / (list) | Each record in the list has included relationships populated. Generates additional queries per record per relationship. Use sparingly on large lists. |
Nested Include
Nested includes (a relationship’s relationship) are NOT supported. Load nested data with separate requests.
Include and Permissions
Included relationships respect the related resource’s list permission. If the user lacks permission, the included relationship returns an empty array (not 403).
16.7 Scopes
Named query filters applied via ?scope=name.
When softDelete: true, three scopes are auto-generated: active (default), deleted, all. Custom scopes are declared in the scopes: block (§20).
Scopes and filters combine with AND — the scope filter applies first, then additional filters narrow the result.
16.8 Field Selection
GET /api/v1/blog/posts?fields=id,title,status,createdAt
Limits which fields appear in the response. Rules:
idis always included regardless of selection.- Virtual computed fields can be included — evaluated for selected fields only.
- Included relationship data is NOT affected by field selection.
- Fields the user lacks read permission for are excluded regardless.
- Hidden fields (
hidden: true) can be included ifrequestable: true.
16.9 Distinct Values
GET /api/v1/blog/posts/distinct?field=status
Returns unique values for a specified field. Only stored fields support distinct. The query respects the current scope and filters. Values are returned in ascending order. Field-level read permissions apply.
16.10 Count
GET /api/v1/blog/posts/count?filter[status]=published
Returns the number of matching records. Respects filters, search, and scopes. Does not use pagination, sorting, include, or field selection parameters.
16.11 Bulk Operations
Bulk endpoints accept arrays of records for batch processing. Always available — no feature flag required.
Bulk Create
POST /api/v1/blog/posts/bulk
[
{ "title": "Post 1", "body": "Content 1" },
{ "title": "Post 2", "body": "Content 2" },
{ "title": "Post 3", "body": "Content 3" }
]
Response (201):
{
"data": {
"created": 3,
"failed": 0,
"items": [ ... ],
"errors": []
}
}
Each record is processed through the full create pipeline: validation, hooks, computed fields, audit. Records that fail validation are reported; remaining records are still processed.
Bulk Update
PATCH /api/v1/blog/posts/bulk
[
{ "id": "uuid-1", "status": "published" },
{ "id": "uuid-2", "status": "published" }
]
Each object MUST include the id field. Records are updated individually through the full update pipeline.
Bulk Delete
DELETE /api/v1/blog/posts/bulk
{ "ids": ["uuid-1", "uuid-2", "uuid-3"] }
For soft-deletable resources, sets deletedAt. For others, permanently removes records.
Limits and Behavior
- Maximum 100 records per bulk request. Exceeding returns 422
BULK_LIMIT_EXCEEDED. - Each record is subject to the same RBAC checks as an individual operation.
- Records are processed independently — a failure on one record does not roll back others.
- If atomic all-or-nothing behavior is required, use a Lua hook or workflow.
16.12 Performance Considerations
N+1 Queries from Include
?include= on list endpoints generates additional queries per record per relationship. For 100 posts with ?include=author,tags: 1 + 100 + 100 = 201 queries.
Mitigations:
- Use
?include=sparingly on list endpoints; reserve for detail views. - Use
?fields=to reduce payload size. - Use
caching: trueon related reference data resources. - A future version MAY implement batch loading to eliminate N+1.
Filter Performance
Filtering on unindexed fields requires full table scans. Declare indexes (§19) for frequently-filtered fields:
indexes:
- fields: [status]
- fields: [authorId, status]
- fields: [createdAt]
Search Performance
SQLite uses LIKE '%term%' (full scan regardless of indexes). PostgreSQL uses tsvector with GIN index (efficient full-text search). For large datasets with search as a core feature, PostgreSQL is strongly recommended.
Count Performance
SELECT COUNT(*) on large tables without indexes can be slow. totalCount in page-based pagination uses the same query. For read-heavy resources, caching: true caches both list results and counts.
Spatial filters (v0.3.1): on PostgreSQL + PostGIS, geometry fields accept
filter[<geom>][bbox]=minLon,minLat,maxLon,maxLat,[intersects]=<GeoJSON>, and[within]=<GeoJSON>, plus?simplify=<tolerance>. See Chapter 41 — GIS & Spatial Data.
End of Chapter 16.
Next: Chapter 17 — API Configuration