Skip to content
metreecaPublic

About

Foundations for client-driven, queryable REST/JSON APIs.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@metreeca/qest

npm

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" in tsconfig.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 vendor with only id and name (not its full state)
  • filtered: price between 50 and 150
  • sorted: by price ascending
  • 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"); @context must 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; @none keys for non-localised values in language maps are not allowed and must be handled using the und (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.

About

Foundations for client-driven, queryable REST/JSON APIs.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages