Skip to main content
Version: v5

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.0

Query 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:

OperatorMeaning
==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&lt=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:

SyntaxBehavior
name==nullConverts to null
name==123Converts to number if attribute is untyped
name==trueConverts to boolean if attribute is untyped
name==number:123Explicit number conversion
name==boolean:trueExplicit boolean conversion
name==string:some%20textKeep as string with URL decode
name==date:2024-01-05T20%3A07%3A27.955ZExplicit 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.

SyntaxReturns
?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.0

Use 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:

ValueMeaning
count=exactThe exact number of matching records. Scans the full matched set, so it is more expensive than the page itself.
count=estimatedA 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:

HeaderExampleDescription
Content-Rangeitems 0-24/1234The 0-based, inclusive range of records returned (start-end) out of the total matching set.
Range-UnititemsThe unit used by Content-Range.
Preference-Appliedcount=exactThe 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 200Content-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.

Added in: v4.3.0

Relationships and Joins

Added in: v4.3.0

Harper 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.0

Access 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