@metreeca/qest
v0.13.0
Published
Foundations for client-driven, queryable REST/JSON APIs.
Maintainers
Readme
@metreeca/qest
Foundations for client-driven, queryable REST/JSON APIs.
@metreeca/qest standardises critical capabilities that vanilla REST/JSON APIs typically lack or implement in ad-hoc, non-portable ways:
- Client-Driven: clients specify what they need, retrieving complex envelopes in a single call
- Queryable: advanced filtering and aggregation, supporting faceted search and analytics
- Localised Content: full support for internationalised content with language-tagged dictionaries
Developers seek these features in frameworks like GraphQL; @metreeca/qest brings them to REST/JSON, achieving:
- Familiar Patterns: standard REST and JSON conventions, no new paradigms to learn
- Simple Clients: no specialised libraries, preprocessors, or code generators
- Automated Servers: model-driven development, dramatically reducing implementation effort
- Standard Caching: compatibility with CDNs and browser caches using standard GET requests
For a formal specification of the data model and query protocol, see the QEST: Queryable REST/JSON APIs memo.
Ecosystem
@metreeca/qest focuses on semantics and core data types, leaving applications free to handle validation, storage, and publishing as they see fit; its standardised data model is the foundation of an integrated ecosystem that delivers a powerful model-driven stack for rapid development of linked data applications:
| Package | Description | |-----------------------------|----------------------------------------------------------------| | @metreeca/qest | Foundations for client-driven, queryable REST/JSON APIs | | @metreeca/blue | Declarative blueprints for model-driven linked data processing | | @metreeca/keep | Turnkey model-driven storage for linked data resources | | @metreeca/gate (upcoming) | Zero-code model-driven endpoints for linked data resources | | @metreeca/tile (upcoming) | Minimalist model-driven UI toolkit for linked data resources |
Installation
npm install @metreeca/qest[!WARNING]
TypeScript consumers must use
"moduleResolution": "nodenext"/"node16"/"bundler"intsconfig.json. The legacy"node"resolver is not supported.
Usage
[!NOTE]
This section introduces essential concepts; for complete coverage, see the API reference:
| Module | Description | |--------------------------|----------------------------------| | @metreeca/qest | Shared options and defaults | | @metreeca/qest/state | Resource state representation | | @metreeca/qest/model | Client-driven resource retrieval |
@metreeca/qest types define payload semantics and formats for standard REST operations:
| Method | Type | Description | |--------|--------------|----------------------------------| | GET | Resource | Resource retrieval | | GET | Resource | Collection retrieval | | GET | Template | Client-driven resource retrieval | | POST | Resource | Resource creation | | PUT | Resource | Complete resource state update | | DELETE | none | Resource deletion |
Resources
A Resource is a property map describing the state of a resource, with optional links to other resources:
GET https://data.example.com/products/123{
"id": "https://data.example.com/products/123",
"name": "Widget",
"category": "Electronics",
"tags": [
"gadget",
"featured"
],
"vendor": "https://data.example.com/vendors/456",
"price": 99.99,
"inStock": true
}The same format is used for complete resource updates:
PUT https://data.example.com/products/123({
name: "Widget",
category: "Electronics",
tags: ["gadget", "premium"],
vendor: "https://data.example.com/vendors/456",
price: 79.99,
// inStock // not included → deleted
});Client-Driven Retrieval
Client-driven retrieval lets clients specify exactly what data to retrieve from both single resources and collections. Expansions and nested queries can be arbitrarily deep: no over-fetching of unwanted fields, no under-fetching requiring additional calls to resolve linked resources.
This is the core contribution of @metreeca/qest: vanilla REST/JSON APIs lack a standard way for clients to control retrieval, forcing them to accept fixed server responses or rely on ad-hoc query parameters. Client-driven retrieval fills this gap, supporting precise control over responses while remaining fully compatible with standard HTTP caching.
[!IMPORTANT]
Client-driven retrieval is fully optional. Servers may provide defaults, typically derived from the underlying data model, preserving standard REST/JSON behaviour while enabling advanced capabilities when needed.
Resources — A Template specifies which properties to retrieve from a single resource and how deeply to expand linked resources.
GET https://data.example.com/products/123?<template>where <template> is the URL-encoded JSON Template:
({
id: {},
name: {},
price: {},
vendor: {
id: {},
name: {},
},
});The response includes only the requested properties, with the linked vendor expanded to show just id and name:
{
"id": "https://data.example.com/products/123",
"name": "Widget",
"price": 99.99,
"vendor": {
"id": "https://data.example.com/vendors/145",
"name": "Acme"
}
}Collections — A collection is reached through the resource that owns it, and constrained there: the entry naming it carries the Criteria for filtering, sorting, and pagination alongside the per-item keys. Those keys are a nested Template, or a Projection for computed aggregates (faceted search and analytics).
GET https://data.example.com/products/?<template>where <template> is the URL-encoded JSON Template whose items entry retrieves the collection and
constrains it:
({
items: {
id: {},
name: {},
price: {},
vendor: {
id: {},
name: {},
},
">=price": 50, // filter: price ≥ 50
"<=price": 150, // filter: price ≤ 150
"^price": "asc", // sort: by price ascending
"#": 25, // limit: 25 results
},
});A single call returns exactly what the client requested:
- projected: product
id,name,price - expanded: linked
vendorwith onlyidandname(not its full state) - filtered:
pricebetween 50 and 150 - sorted: by
priceascending - paginated: up to 25 results
{
"items": [
{
"id": "https://data.example.com/products/456",
"name": "Gadget",
"price": 59.99,
"vendor": {
"id": "https://data.example.com/vendors/145",
"name": "Acme"
}
},
{
"id": "https://data.example.com/products/123",
"name": "Widget",
"price": 99.99,
"vendor": {
"id": "https://data.example.com/vendors/145",
"name": "Acme"
}
},
{
"id": "https://data.example.com/products/789",
"name": "Gizmo",
"price": 129.99,
"vendor": {
"id": "https://data.example.com/vendors/236",
"name": "Globex"
}
}
]
}Analytics — A Projection replaces the nested template with computed property bindings, enabling faceted search and analytics in a single call.
GET https://data.example.com/products/?<template>where <template> groups products by category, counting each group and sorting by count:
({
items: {
"category=category": {}, // grouping column (non-aggregate)
"count=count:": {}, // count per group
"^count:": "desc", // sort groups by count descending
},
});A single call returns pre-aggregated category counts, ready for UI faceting or analytics dashboards:
{
"items": [
{
"category": "Electronics",
"count": 150
},
{
"category": "Home",
"count": 89
},
{
"category": "Garden",
"count": 34
}
]
}Localised Properties
Resource properties can hold localised text in a dictionary: a language map associating BCP 47 language tags with text values:
{
"id": "https://data.example.com/products/123",
"name": {
"en": "Widget",
"fr": "Bidule"
},
"description": {
"en": [
"Compact",
"Durable"
],
"fr": [
"Compact",
"Résistant"
]
}
}A Dictionary supports both single-valued and multi-valued forms per language. Within a single
dictionary, all values must be uniformly scalar or uniformly array, in the form the property declares: a state carrying
the other form under any tag is rejected as malformed. Language-neutral values are tagged with the
und (Undetermined) language tag:
({
name: { und: "Widget" },
tags: { und: ["compact", "durable"] },
});Projections can target localised properties through tag-range placeholders. Each row carries a complete Dictionary
value (the entries for the tags the binding's pattern matches) rather than fanning out one row per tag:
({
items: {
"id=id": {},
"label=title": { "*": {} }, // all matching tags, as one Dictionary
},
});Localised properties can also be filtered and sorted through their coalesced label: the value, or values, resolved
from the dictionary by a request-level language priority (derived server-side, for example from
Accept-Language). Search (~) acts on the resolved label, and sort order (^) and focus (+) on its single-valued
form; the priority is supplied out of band, never in the query. See the memo for the full semantics.
JSON-LD Foundations
JSON-LD (JSON for Linked Data) is a W3C standard for publishing linked data on the web. It extends JSON with web identifiers (IRIs) to link resources across systems and domains, and to give property names precise, machine-readable meaning by mapping them to shared vocabularies, a capability at the heart of the Web Data Activity (Semantic Web) and modern knowledge graphs.
@metreeca/qest defines a controlled JSON-LD subset designed to feel like plain idiomatic JSON, letting JavaScript developers work with linked data using familiar REST/JSON patterns without mastering JSON-LD technicalities, while retaining full compatibility with standard JSON-LD processors.
This controlled subset is specified by:
- compacted documents with short property names and nested objects, just like regular JSON
- ECMAScript identifiers as property names
(terms), enabling dot notation access;
JSON-LD keywords (
@id,@type, etc.) and blank node identifiers are not allowed and must be mapped to identifiers via an application-provided@context(for example,"id": "@id");@contextmust also map property names to IRIs for semantic interoperability - native JSON primitives (
boolean,number,string) as values; typed literals with arbitrary datatypes are not allowed and must be represented as strings with datatype coercion declared in@context - language maps for localised text;
@nonekeys for non-localised values in language maps are not allowed and must be handled using theund(Undetermined) language tag - IRI references for linking resources across systems and domains; data structures require absolute IRIs; codec functions handle conversion to/from root-relative forms
Support
- Open an issue to report a problem or to suggest a new feature
- Start a discussion to ask a how-to question or to share an idea
License
This project is licensed under the Apache 2.0 License. See LICENSE for details.
