REST Querying
Harper's REST interface supports a rich URL-based query language for filtering, sorting, selecting, and limiting records. Queries are expressed as URL query parameters on collection paths.
Basic Attribute Filtering
Search by attribute name and value using query parameters. The queried attribute must be indexed.
GET /Product/?category=software
Multiple attributes can be combined — only one needs to be indexed for the query to execute:
GET /Product/?category=software&inStock=true
Null Queries
Added in: v4.3.0Query for null values or non-null values:
GET /Product/?discount=null
Note: Only indexes created in v4.3.0 or later support null indexing. Existing indexes must be rebuilt (removed and re-added) to support null queries.
Comparison Operators (FIQL)
Harper uses FIQL syntax for comparison operators:
| Operator | Meaning |
|---|---|
== | Equal |
=lt= | Less than |
=le= | Less than or equal |
=gt= | Greater than |
=ge= | Greater than or equal |
=ne=, != | Not equal |
=ct= | Contains (strings) |
=sw=, ==<value>* | Starts with (strings) |
=ew= | Ends with (strings) |
=, === | Strict equality (no type conversion) |
!== | Strict inequality (no type conversion) |
Examples:
GET /Product/?price=gt=100
GET /Product/?price=le=20
GET /Product/?name==Keyboard*
GET /Product/?category=software&price=gt=100&price=lt=200
For date fields, colons must be URL-encoded as %3A:
GET /Product/?listDate=gt=2017-03-08T09%3A30%3A00.000Z
Chained Conditions (Range)
Omit the attribute name on the second condition to chain it against the same attribute:
GET /Product/?price=gt=100<=200
Chaining supports gt/ge combined with lt/le for range queries. No other chaining combinations are currently supported.
Type Conversion
For FIQL comparators (==, !=, =gt=, etc.), Harper applies automatic type conversion:
| Syntax | Behavior |
|---|---|
name==null | Converts to null |
name==123 | Converts to number if attribute is untyped |
name==true | Converts to boolean if attribute is untyped |
name==number:123 | Explicit number conversion |
name==boolean:true | Explicit boolean conversion |
name==string:some%20text | Keep as string with URL decode |
name==date:2024-01-05T20%3A07%3A27.955Z | Explicit Date conversion |
If the attribute specifies a type in the schema (e.g., Float), values are always converted to that type before searching.
For strict operators (=, ===, !==), no automatic type conversion is applied — the value is decoded as a URL-encoded string, and the attribute type (if declared in the schema) dictates type conversion.
Unions (OR Logic)
Use | instead of & to combine conditions with OR logic:
GET /Product/?rating=5|featured=true
Grouping
Use parentheses or square brackets to control order of operations:
GET /Product/?rating=5|(price=gt=100&price=lt=200)
Square brackets are recommended when constructing queries from user input because standard URI encoding safely encodes [ and ] (but not ():
GET /Product/?rating=5&[tag=fast|tag=scalable|tag=efficient]
Constructing from JavaScript:
let url = `/Product/?rating=5&[${tags.map(encodeURIComponent).join('|')}]`;
Groups can be nested for complex conditions:
GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inStock=true]
Query Functions
Harper supports special query functions using call syntax, included in the query string separated by &.
select(properties)
Specify which properties to include in the response.
| Syntax | Returns |
|---|---|
?select(property) | Values of a single property directly |
?select(property1,property2) | Objects with only the specified properties |
?select([property1,property2]) | Arrays of property values |
?select(property1,) | Objects with a single specified property |
?select(property{subProp1,subProp2}) | Nested objects with specific sub-properties |
Examples:
GET /Product/?category=software&select(name)
GET /Product/?brand.name=Microsoft&select(name,brand{name})
limit(end) or limit(start,end)
Limit the number of results returned, with an optional starting offset.
GET /Product/?rating=gt=3&inStock=true&select(rating,name)&limit(20)
GET /Product/?rating=gt=3&limit(10,30)
sort(property) or sort(+property,-property,...)
Sort results by one or more properties. Prefix + or no prefix = ascending; - = descending. Multiple properties break ties in order.
GET /Product/?rating=gt=3&sort(+name)
GET /Product/?sort(+rating,-price)
Pagination and Total Count
Added in: v5.3.0Use limit(start,end) to page through a collection, and opt in to a total match count with the Prefer request header (Prefer: count=exact) so a client can render pagination (for example "1-25 of 1,234") without a second request.
Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to GET/HEAD requests and requires a bounded page — the limit() must be a non-negative integer no larger than 10,000, and the requested window (offset + limit) no larger than 1,000,000. A request with no limit(), one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging.
Requesting a count
Send a Prefer header on a GET (or HEAD) request to a collection:
| Value | Meaning |
|---|---|
count=exact | The exact number of matching records. Scans the full matched set, so it is off by default (see below) and served as an estimate unless enabled for the REST interface. |
count=estimated | A fast planner/table estimate. Cheap, approximate. |
GET /Product/?category=software&limit(0,25)
Prefer: count=exact
Count response headers
The count is returned in RFC 7233-style response headers alongside the page body:
| Header | Example | Description |
|---|---|---|
Content-Range | items 0-24/1234 | The 0-based, inclusive range of records returned (start-end) out of the total matching set. |
Range-Unit | items | The unit used by Content-Range. |
Preference-Applied | count=exact | The count mode the server applied (exact or estimated). |
HTTP/1.1 200 OK
Content-Range: items 0-24/1234
Range-Unit: items
Preference-Applied: count=exact
The response status is always 200 — Content-Range is informational (Harper does not use 206 Partial Content). A HEAD request with a count preference returns the count headers with no body — it saves transferring the page, but count=exact still scans the matched set (subject to the same guardrails), so it is a bandwidth-saving pre-flight, not a low-cost counting shortcut. When CORS is enabled, these three headers are added to Access-Control-Expose-Headers so browser clients can read them cross-origin.
Unavailable totals
The total is reported as * (for example Content-Range: items 0-24/*) when it cannot be produced — an exact scan that hits its guardrail (counting the tail past the requested page is bounded by a 1,000,000-row cap and a ~1-second budget, and abandons the total rather than truncating the page), or a query with no cardinality estimate (for example a != or =ct= (contains) condition). Preference-Applied still echoes the requested mode, so an unavailable total (.../*) is distinct from a request that asked for no count.
Enabling exact counts
Because an exact count scans the full matched set, it is off by default. Enable it in an application's REST configuration via the exactCount option:
rest:
exactCount: true
Without it, a count=exact request is served as an estimate (the response reports Preference-Applied: count=estimated). Estimated counts do not require exactCount; when no estimate is available, the total is reported as *.
Relationships and Joins
Added in: v4.3.0Harper supports querying across related tables through dot-syntax chained attributes. Relationships must be defined in the schema using @relationship.
Schema example:
type Product @table @export {
id: Long @primaryKey
name: String
brandId: Long @indexed
brand: Brand @relationship(from: "brandId")
}
type Brand @table @export {
id: Long @primaryKey
name: String
products: [Product] @relationship(to: "brandId")
}
Query by related attribute (INNER JOIN behavior):
GET /Product/?brand.name=Microsoft
GET /Brand/?products.name=Keyboard
Nested Select with Joins
Relationship attributes are not included by default. Use select() to include them:
GET /Product/?brand.name=Microsoft&select(name,brand)
GET /Product/?brand.name=Microsoft&select(name,brand{name})
GET /Product/?name=Keyboard&select(name,brand{name,id})
When selecting without a filter on the related table, this acts as a LEFT JOIN — the relationship property is omitted if the foreign key is null or references a non-existent record.
Many-to-Many Relationships
Many-to-many relationships can be modeled with an array of foreign key values, without a junction table:
type Product @table @export {
id: Long @primaryKey
name: String
resellerIds: [Long] @indexed
resellers: [Reseller] @relationship(from: "resellerIds")
}
GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
The array order of resellerIds is preserved when resolving the relationship.
Property Access via URL
Changed in: v4.5.0Access a specific property of a record by appending it with dot syntax to the record id:
GET /MyTable/123.propertyName
This only works for declared properties — a table's schema attributes, or a programmatic Resource's static properties Changed in: v5.2.0. As of v4.5.0, dots in URL paths are no longer interpreted as property access for undeclared properties, allowing URLs to generally include dots without being misinterpreted. The suffixes .json, .cbor, .msgpack, and .csv are reserved as content-type selectors and take precedence over a property of the same name.
directURLMapping Option
Added in: v4.5.0
Resources can be configured with directURLMapping: true for more direct URL path handling. When enabled, the URL path is mapped more directly to the resource without the default query parameter parsing semantics. See Database / Schema for configuration details.
See Also
- REST Overview — HTTP methods, URL structure, and caching
- Headers — Request and response headers
- Content Types — Encoding formats
- Database / Schema — Defining schemas, relationships, and indexes