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 also requires a limit() — a count request on an unbounded collection (no limit()) is served normally, with no count headers, since counting the whole collection 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 more expensive than the page itself. |
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 Prefer: count= returns the count headers with no body, a cheap way to ask "how many match?" without transferring the page. 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 reaches its internal work limit, 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.
Disabling exact counts
Because an exact count scans the full matched set, you can disable it in an application's REST configuration via the exactCount option. With exactCount: false, a count=exact request is served as an estimate instead (the response reports Preference-Applied: count=estimated). Estimated counts are always available; the default is true.
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 properties declared in the schema. 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.
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