Quickback Docs
Quickback for Hono API

Query Parameters

Filter, paginate, sort, search, and select fields in API responses. Reference for all query parameter operators.

The generated API supports filtering, pagination, sorting, field selection, search, and total count via query parameters on GET list endpoints.

Filter Operators

OperatorQuery ParamSQL Equivalent
Equals?field=valueWHERE field = value
Not equals?field.ne=valueWHERE field != value
Greater than?field.gt=valueWHERE field > value
Greater or equal?field.gte=valueWHERE field >= value
Less than?field.lt=valueWHERE field < value
Less or equal?field.lte=valueWHERE field <= value
Pattern match?field.like=valueWHERE field LIKE '%value%'
In list?field.in=a,b,cWHERE field IN ('a','b','c')

Examples

# Filter by status
GET /api/v1/jobs?status=open

# Range query
GET /api/v1/jobs?salaryMin.gte=100000&salaryMax.lte=200000

# Pattern matching
GET /api/v1/jobs?title.like=Engineer

# Multiple values
GET /api/v1/jobs?department.in=Engineering,Design,Product

Pagination

ParameterDefaultDescription
limit50Number of records to return (min: 1, max: 100)
offset0Number of records to skip
GET /api/v1/jobs?limit=25&offset=50

The default limit can be configured per-resource in your read definition:

read: {
  access: { roles: ["recruiter"] },
  pageSize: 25,       // Default limit
  maxPageSize: 100,   // Maximum allowed limit
},

Response Shape

{
  "data": [ /* records */ ],
  "pagination": {
    "limit": 25,
    "offset": 50,
    "count": 12
  }
}
  • count — number of records returned on this page
  • total — total matching records across all pages (only when ?total=true, see below)

Sorting

Sort by one or more fields. Use the - prefix for descending order.

# Sort by status ascending, then createdAt descending
GET /api/v1/jobs?sort=status,-createdAt

# Single field descending
GET /api/v1/jobs?sort=-createdAt

# Multiple fields
GET /api/v1/jobs?sort=department,-salaryMax,title
PrefixDirection
(none)Ascending
-Descending

Legacy Format

The original sort + order format is still supported for backwards compatibility:

GET /api/v1/jobs?sort=title&order=asc
ParameterValuesDefaultDescription
sortAny column namecreatedAtField to sort by
orderasc, descdescSort direction

When the multi-sort format is detected (comma or - prefix), the order parameter is ignored.

Field Selection

Select which columns to return using ?fields=. Available on LIST and GET routes (not Views — they define their own field set).

# Return only id, title, and status
GET /api/v1/jobs?fields=id,title,status

# Combine with other query params
GET /api/v1/jobs?fields=id,title,status&status=open&sort=-createdAt

# Single record
GET /api/v1/jobs/job_123?fields=id,title,salaryMin,salaryMax

All columns are available including system columns (id, organizationId, createdAt, modifiedAt, etc.). Invalid field names are silently ignored. If no valid fields are provided, all columns are returned.

Security: Masking still applies to selected fields. Requesting ?fields=ssn will return the masked value, not the raw data.

Total Count

Get the total number of matching records across all pages by adding ?total=true. Available on LIST and VIEW routes.

GET /api/v1/jobs?status=open&total=true
{
  "data": [ /* 25 records */ ],
  "pagination": {
    "limit": 25,
    "offset": 0,
    "count": 25,
    "total": 142
  }
}

This is opt-in because it runs an additional COUNT(*) query. Only use it when you need the total (e.g., for pagination UI).

Full-text search across all text columns using ?search=. Available on LIST and VIEW routes.

# Search across all text fields
GET /api/v1/jobs?search=engineer

# Combine with filters
GET /api/v1/jobs?search=engineer&status=open

The search generates an OR'd LIKE condition across all text() columns in your schema:

WHERE (title LIKE '%engineer%' OR department LIKE '%engineer%')

Only columns defined with text() in your Drizzle schema are searchable. Non-text columns (integers, timestamps, UUIDs, blobs) are automatically excluded.

Complete Example

Combine all query parameters together:

GET /api/v1/jobs?fields=id,title,status,salaryMin&status=open&salaryMin.gte=100000&search=engineer&sort=salaryMin,-createdAt&limit=10&offset=20&total=true

This request:

  1. Selects only id, title, status, salaryMin fields
  2. Filters to open jobs with salaryMin >= 100000
  3. Searches text columns for "engineer"
  4. Sorts by salaryMin ascending, then createdAt descending
  5. Paginates with 10 results starting at offset 20
  6. Counts total matching records

Parameter Summary

ParameterApplies ToDescription
limitLIST, VIEWPage size (default: 50, max: 100)
offsetLIST, VIEWSkip N records
sortLIST, VIEWSort fields (comma-separated, - prefix for desc)
orderLIST, VIEWLegacy sort direction (asc or desc)
fieldsLIST, GETComma-separated column names to return
totalLIST, VIEWSet to true to include total count
searchLIST, VIEWSearch text across all text columns
field=valueLIST, VIEWFilter by exact match
field.op=valueLIST, VIEWFilter with operator (gt, gte, lt, lte, ne, like, in)
starting_afterLIST, VIEWOpaque keyset cursor — rows strictly after the boundary
ending_beforeLIST, VIEWOpaque keyset cursor — rows strictly before the boundary
includeLIST, VIEW, GETFK-graph embedding (contract v2, allowlisted — see below)
fields[<fk>]LIST, VIEW, GETSparse fields for an embedded include target (contract v2)

Include & Sparse Fields (contract v2)

On the v2 contract a resource can allowlist FK columns for FK-graph embedding, collapsing client-side N+1 fetches:

quickback/features/orders/order-items.ts
export default feature("orderItems", {
  columns: {
    id:             q.id(),
    orderId:        q.text().required().references(() => orders.id),
    organizationId: q.scope("organization"),
  },
  read: {
    access: { roles: ["member", "admin"] },
    include: ["orderId"],          // FK columns eligible for ?include=
  },
});
GET /api/v2/order-items?include=orderId&fields[orderId]=id,total
{
  "data": [ { "id": "oi_1", "orderId": "ord_9" } ],
  "included": { "orderId": { "ord_9": { "id": "ord_9", "total": 129 } } }
}

Embedded rows land in a top-level included map keyed by FK column, then by target primary key — row objects are never mutated.

Security semantics (all enforced, fail-closed):

  • An embedded read is a read of the target resource: the target's own read.access is checked before any fetch (403 on failure, never silent omission), the child query runs under the target's tenant firewall on the caller's own database handle, and embedded rows pass through the target's masking before attachment.
  • Targets whose read.access is record-aware (function access, record: conditions, relationship or fga arms) are refused at compile time — embedding them would skip their post-fetch record checks.
  • A tenant-scoped target with no resolvable firewall clause is a compile error; a global reference table must opt out explicitly with firewall: { exception: true }. Even then, the child fetch still calls the target's own firewall helper — for a soft-deleting reference table that helper is the soft-delete predicate, so an embed never serves rows the target's own routes refuse.
  • ?include= on a resource (or view) without an allowlist is a 400. Explicit views declare their own include: [...] — nothing is expandable undeclared. Depth is exactly 1. A view that declares include must keep each listed FK column in its fields projection (compile error otherwise — the embedded lookup pivots on that column).

Strict fields on v2: unknown names in bare ?fields= and in fields[<fk>]= are rejected with a 400 Problem. (Pinned v1 keeps its historical silent-drop for bare ?fields=.) A ?fields= projection that drops the FK column of a requested ?include= is also a 400include=orderId requires "orderId" in ?fields= — never a silently-empty included slot.

On this page