Ga naar hoofdinhoud

Resources

Introduction​

The API of an integration layer is centered around resources. A resource represents a 'thing' of a certain type. It can correspond to anything — from a physical object (e.g. a building or a person) to an abstract concept (e.g. a collection or a type of art work).

Data model​

This specification defines resource types on several pages. This page describes the generic types from which all other resource types extend:

NameDescription
ResourceA 'thing' of a certain type. All other resource types extend from it. It is the umbrella term for everything the integration layer publishes and a presentation layer can interact with. A Resource is abstract: there is no resource whose type is Resource — it needs a concrete type. This specification defines a number of concrete types. The integration layer may additionally define its own types.
CollectionAn ordered list of resources. A Collection may contain further collections, and may consist of pages, containing sublists of the resources in the collection.
PageAn ordered sublist of resources within a Collection.

The following class diagram visualises the relationships between the resource types:

The Collections, Entities, Facets and Suggestions pages define the resource types that specialise them.

Resource​

A Resource, regardless of type, contains at least the following properties:

NameData typeCardinalityDescription
typestring1The type of the resource. This specification defines a number of types. The integration layer may additionally define its own types.
idstring0 or 1The identifier of the resource. It MUST be a dereferenceable HTTP URI. Optional for volatile, non-persistent resources, such as Keyword Values or Facet Values.
namestring0 or 1The name of the resource, if known.

The integration layer MUST include type in the response body of every resource it returns, and id and name when they are known. The combination of these three properties allow a presentation layer to identify a resource.

Example​

Example of the response body of the integration layer:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/entities/1234",
"type": "HeritageObject",
"name": "The Night Watch",
// And then, depending on the resource, other properties, such as:
"additionalTypes": [
{
"id": "https://example.org/v1/entities/5678",
"type": "Concept",
"name": "Painting"
}
],
"description": "Rembrandt’s largest, most famous canvas was made for the Arquebusiers guild hall..."
}

The response indicates that this resource has identifier https://example.org/v1/entities/1234, is a HeritageObject and has name 'The Night Watch'.

Note the additionalTypes list: every item in this list is also a resource and has the same top-level properties: id, type and name.

Collection​

A Collection contains at least the following properties:

NameData typeCardinalityDescription
idstring1The identifier of the collection.
typestring1The type of the collection. It MUST be Collection or a specialisation.
namestring1The name of the collection.
totalItemsnumber0 or 1The exact total number of items in the collection, being its further collections, its resources, or both. The property MAY be omitted by the integration layer if the exact total is too costly to calculate. Mutually exclusive with estimatedTotalItems.
estimatedTotalItemsnumber0 or 1An estimate of the total number of items in the collection. It can be higher or lower than the real total. The property MAY be omitted by the integration layer if it has no estimate. Mutually exclusive with totalItems.
itemsarray0 or 1A list of the items in the collection. Not set if the Collection is divided into pages.
items[*]Resource1A resource. A resource can be of any type, including Collection or a specialisation.
firstPage0 or 1The first page in the collection. Not set if the collection is empty or if it is not divided into pages.
first.idstring1The identifier of the first page in the collection.
first.typestring1The type of the first page in the collection. It MUST be Page or a specialisation.
lastPage0 or 1The last page in the collection. Not set if the collection is empty, if the collection is not divided into pages, or if the last page is unknown (e.g. in case of cursor navigation).
last.idstring1The identifier of the last page in the collection.
last.typestring1The type of the last page in the collection. It MUST be Page or a specialisation.
partOfCollection0 or 1The collection of which this collection is a part. Not set if this collection is the root of a collection tree.
partOf.idstring1The identifier of the collection.
partOf.typestring1The type of the collection. It MUST be Collection or a specialisation.
partOf.namestring1The name of the collection.
belongsToCollection0 or 1The collection that the collections in this collection belong to, such as a list of facet collections that belongs to a curated collection. Not set if this collection does not group collections of another collection.
belongsTo.idstring1The identifier of the collection.
belongsTo.typestring1The type of the collection. It MUST be Collection or a specialisation.
belongsTo.namestring1The name of the collection.
facetsCollection0 or 1The collection of this collection's facet collections. Not set if the collection does not offer facets, or if the collection is a facet collection: a facet collection MUST NOT offer facets itself. See Facets.
facets.idstring1The identifier of the collection.
facets.typestring1The type of the collection. It MUST be Collection or a specialisation.
facets.namestring1The name of the collection.
suggestionsCollection0 or 1The collection of this collection's suggestion collections. Not set if the collection does not offer suggestions, or if the collection is a suggestion collection: a suggestion collection MUST NOT offer suggestions itself. See Suggestions.
suggestions.idstring1The identifier of the collection.
suggestions.typestring1The type of the collection. It MUST be Collection or a specialisation.
suggestions.namestring1The name of the collection.
capabilitiesarray0 or 1The URIs of the capabilities the integration layer implements for this collection. MUST be omitted if the collection supports no capabilities. See Capability discovery.

The collection tree​

Collections form a tree: the items of a collection can contain further collections. There is no separate type for a 'collection of collections': a collection that groups other collections is a collection like any other. This keeps the structure a presentation layer has to learn down to one collection type.

Every collection tree has one root: the collection that has no partOf. Every other collection in that tree MUST have a partOf. This makes the tree connected and every collection in it reachable from its root. A collection MUST NOT be part of itself, directly or indirectly: the tree MUST be acyclic, so that a presentation layer can traverse it without looping.

An integration layer MAY offer more than one collection tree. The facet collections and suggestion collections of a collection, for example, form trees of their own. Their root is not part of the collection tree of that collection — it has no partOf and uses belongsTo instead.

A presentation layer walks a tree from its root. It requests the root, and for every item in a collection it recurses if the item's type is Collection or a specialisation of it, and renders the item as a resource otherwise. It does not need to know in advance whether a collection holds further collections or resources, or whether the collections it encounters are divided into pages. Every collection, including one that groups other collections, can be divided into pages, so a tree with many branches can be traversed a page at a time.

Collection paths​

Every collection has a path: the URI a presentation layer uses to request it.

This specification defines the path of some collections itself:

PathCollection
/{version}/collectionsThe root of the curated collection tree.
/{version}/collections(/{...collections})/{collection}A curated collection.
/{version}/{...collections}/facetsThe facet collections of a collection.
/{version}/{...collections}/facets/{facet}One facet collection.
/{version}/{...collections}/suggestionsThe suggestion collections of a collection.
/{version}/{...collections}/suggestions/{suggestion}One suggestion collection.

The words collections, facets and suggestions are reserved: a path identifier MUST NOT be one of them. A path like /{version}/collections/facets/facets would otherwise match two of the paths above at once: a curated collection 'facets' inside a curated collection 'facets', and the facet collections of a curated collection 'facets'. Neither the integration layer nor a presentation layer can then say which one a URI means.

A presentation layer does not have to build these paths. It finds the facet collections and the suggestion collections of a collection in the facets and suggestions properties, which hold the full URI of each list. The fixed paths tell an integration layer where to put those lists, and give a presentation layer a URI it can link to.

The path identifiers themselves are a choice of the integration layer: it names each of its collections, facet collections and suggestion collections itself. See resource identifiers. An integration layer MAY expose the same collection at more than one path.

Example​

Example of the response body of the integration layer's root collection:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections",
"type": "Collection",
"name": "Collections",
"totalItems": 2,
"items": [
{
"id": "https://example.org/v1/collections/objects",
"type": "Collection",
"name": "Heritage objects"
},
{
"id": "https://example.org/v1/collections/persons",
"type": "Collection",
"name": "Persons"
}
]
}

The response indicates that the root groups two further collections, 'Heritage objects' and 'Persons'.

Example of the response body when the collection is divided into pages:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/objects",
"type": "Collection",
"name": "Heritage objects",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/objects?page=1",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections",
"type": "Collection",
"name": "Collections"
},
"capabilities": ["https://specs.nde.nl/rest/v1/page-pagination"]
}

The response indicates that this collection consists of 195 items, accessible via the first page.

Capability discovery​

A collection can do more than return a list of items. It can support a keyword search, filters, facets, suggestions or pagination. Each of these is a capability: something that a collection supports. Every collection advertises its own capabilities, so that a presentation layer can discover what a collection supports and adapt its user interface to it.

This specification defines the following capabilities:

Capability URIDescription
https://specs.nde.nl/rest/v1/keyword-searchThe collection supports keyword search.
https://specs.nde.nl/rest/v1/keyword-filterThe collection supports keyword filters.
https://specs.nde.nl/rest/v1/date-filterThe collection supports date filters.
https://specs.nde.nl/rest/v1/numeric-filterThe collection supports numeric filters.
https://specs.nde.nl/rest/v1/geolocation-filterThe collection supports geolocation filters.
https://specs.nde.nl/rest/v1/facet-filterThe collection supports filters on its facets.
https://specs.nde.nl/rest/v1/facetsThe collection supports facets.
https://specs.nde.nl/rest/v1/suggestionsThe collection supports suggestions.
https://specs.nde.nl/rest/v1/highlightingThe collection supports text highlighting in string properties matching a keyword query.
https://specs.nde.nl/rest/v1/page-paginationThe collection supports pagination by page numbers.
https://specs.nde.nl/rest/v1/cursor-paginationThe collection supports pagination by a cursor.

Capabilities are a property of a single collection. A presentation layer MUST NOT infer the capabilities of a collection from the capabilities of its ancestors in the collection tree.

The capabilities property tells a presentation layer that a collection supports something. It does not tell it how to use it: the endpoints and query parameters of the collection do that.

Two capabilities come with a property that point to a collection. The facets property of a collection points to its list of facet collections; see Facets. The suggestions property of a collection points to its list of suggestion collections; see Suggestions. The other capabilities have no such property. A presentation layer finds them in this specification: q for a keyword search, filter for a filter of any type, and page for a page.

Filters​

A presentation layer can narrow down the items of a collection with filters, using the filter query parameter. An integration layer MAY offer filters for any collection. Which filters a collection supports is a choice of the integration layer.

The filters a collection supports depend on the resources it holds. A curated collection of entities with geographical data may support a geolocation filter. A facet collection does not, because its items are facet items rather than entities with a location.

This specification describes the following filter types:

  1. Keyword filter: filtering on text, with wildcards and phrases (e.g. Rem, Rem*, 'Rembrandt van Rijn').
  2. Date filter: filtering by comparing dates (e.g. 'Date of creation is between 1900 and 1950').
  3. Numeric filter: filtering by comparing numbers (e.g. 'Number of pages is at least 300').
  4. Geolocation filter: filtering on coordinates and a radius (e.g. 'Location of creation is within 25 km of a geopoint').
  5. Facet filter: filtering by the values of one of the collection's facets (e.g. 'Creator is "Rembrandt" or "Vincent van Gogh" and Type is "Painting"').

The list is not fixed. An integration layer may add filter types of its own for specific use cases. It advertises every filter type it supports for a collection as a capability; see Capability discovery.

A presentation layer MUST only send filters that the collection supports. If it sends another filter, the integration layer MUST respond with a 400 status code. Ignoring an unsupported filter would silently give the presentation layer more items than it asked for.

To be discussed

Is there a standard or common notation to express filter and facet parameters via a query string?

Options could be Feed Item Query Language (FIQL), RSQL or OData. These can be heavy-weight, though, or be unable to express all parameters (e.g. facets that should be retrieved). Alternatively, use a custom notation using a convention, e.g. the LHS bracket syntax, that can be mapped to JSON for processing by the integration layer? For example:

  1. Filter by range: date of creation is between 1900 and 1950

GET /v1/collections/masterpieces?page=1&filter[dateCreated][gte]=1900&filter[dateCreated][lte]=1950

  1. Filter by geolocation: location of creation is within 25 km of geopoint 52.0752021, 5.1135515

GET /v1/collections/masterpieces?page=1&filter[locationCreated][lat]=52.0752021&filter[locationCreated][distance][lon]=5.1135515&filter[locationCreated][distance][radius]=25km

  1. Filter by facet: creator ID is 'https://example.org/v1/entities/7890' or 'https://example.org/v1/entities/9012'

GET /v1/collections/masterpieces?page=1&filter[creators][in]=https://example.org/v1/entities/7890&filter[creators][in]=https://example.org/v1/entities/9012

  1. Instruct the integration layer to return a maximum of 5 facet values of facet 'Creator', and that these values must be ordered by count and then by name

GET /v1/collections/masterpieces?page=1&facet[creators][orderBy][count]=desc&facet[creators][orderBy][name]=asc&facet[creators][size]=5

To do

Think of a way to express the ID of a facet in the query string. A facet ID like creators is a shorthand for its full URI but currently does not have a designated property in a facet collection. Full URIs — such as https://example.org/v1/collections/masterpieces/facets/creators — are rather verbose.

Page​

A Page contains at least the following properties:

NameData typeCardinalityDescription
idstring1The identifier of the page.
typestring1The type of the page. It MUST be Page or a specialisation.
namestring1The name of the page.
itemsarray1A list of resources in the page. Empty if there are no resources.
items[*]Resource1A resource. A resource can be of any type.
prevPage0 or 1The previous page in the collection. Not set if there is no previous page.
prev.idstring1The identifier of the previous page in the collection.
prev.typestring1The type of the previous page in the collection. It MUST be Page or a specialisation.
nextPage0 or 1The next page in the collection. Not set if there is no next page.
next.idstring1The identifier of the next page in the collection.
next.typestring1The type of the next page in the collection. It MUST be Page or a specialisation.
partOfCollection1The collection to which the items contained by the page belong.
partOf.idstring1The identifier of the collection.
partOf.typestring1The type of the collection. It MUST be Collection or a specialisation.
partOf.namestring1The name of the collection.
partOf.totalItemsnumber0 or 1The exact total number of items in the collection, being its further collections, its resources, or both. The property MAY be omitted by the integration layer if the exact total is too costly to calculate. Mutually exclusive with partOf.estimatedTotalItems.
partOf.estimatedTotalItemsnumber0 or 1An estimate of the total number of items in the collection. It can be higher or lower than the real total. The property MAY be omitted by the integration layer if it has no estimate. Mutually exclusive with partOf.totalItems.
partOf.firstPage1The first page in the collection.
partOf.first.idstring1The identifier of the first page in the collection.
partOf.first.typestring1The type of the first page in the collection. It MUST be Page or a specialisation.
partOf.lastPage0 or 1The last page in the collection. Not set if the last page is unknown (e.g. in case of cursor navigation).
partOf.last.idstring1The identifier of the last page in the collection.
partOf.last.typestring1The type of the last page in the collection. It MUST be Page or a specialisation.

Pagination​

Pagination means splitting the items of a collection into smaller lists. Each smaller list is a Page. A Page is a resource of its own: it has its own identifier, and a presentation layer requests it like any other resource.

An integration layer needs pagination when its collections are large. A single collection may contain hundreds of thousands of items. It is impractical to return all of them in one response: the response takes long to build and long to download, and a presentation layer usually shows only a few items at a time. With pagination, the integration layer returns a small number of items per response, and the presentation layer asks for more only when it needs more.

An integration layer MAY divide any collection into pages. It does not have to. A collection MAY return all of its items at once, in its items property.

Pages and collections​

A collection that is divided into pages has two properties that point to its pages:

  • first: the first page of the collection.
  • last: the last page of the collection, if the integration layer knows it.

A page has three properties that point to its place in the collection:

  • prev: the page before it, if there is one.
  • next: the page after it, if there is one.
  • partOf: the collection that the page belongs to.

With these properties, a presentation layer walks through a collection. It requests the collection and follows first. It then follows next until there is no next left, which means it has reached the end. It goes back with prev, or it jumps to the end with the collection's last.

The prev and the next of a page MUST be pages of the same collection. They MUST also point back at each other: if the next of one page is another page, the prev of that other page MUST be the first one. The pages of a collection MUST form one sequence without loops, so that a presentation layer can walk through them without getting stuck. The first of a collection MUST be the page that has no prev. Its last, if it is set, MUST be the page that has no next.

The following diagram visualises the relationships:

Pagination means that a collection is split into pages. Navigation is how a presentation layer moves through those pages. An integration layer uses one of two pagination strategies: page navigation and cursor navigation.

A presentation layer asks for a page with the page query parameter. The value of page is either a number or a cursor:

  • Page navigation: the value is a number that says which page the presentation layer wants, such as ?page=3.
  • Cursor navigation: the value is a token that the integration layer has given itself, such as ?page=eyJpZCI6MTIzfQ. The token points to a place in the collection.

Both are allowed. An integration layer MAY use page navigation, MAY use cursor navigation, and MAY use both across its collections. For predictable navigation, it is RECOMMENDED that an integration layer uses the same strategy for all of its collections, so that a presentation layer can handle every collection in one way.

An integration layer MUST make its pagination strategy explicit. A collection that is divided into pages MUST advertise exactly one of https://specs.nde.nl/rest/v1/page-pagination and https://specs.nde.nl/rest/v1/cursor-pagination in its capabilities property, and MUST NOT advertise both. A collection that is not divided into pages MUST NOT advertise either of them. There is no default: a presentation layer MUST NOT assume a pagination strategy for a collection that advertises neither.

Counting all items of a collection costs time. Page navigation usually needs that number to know which page comes last, and cursor navigation usually does not. An integration layer that uses page navigation MAY return an exact totalItems, and an integration layer that uses cursor navigation MAY return an estimatedTotalItems. An integration layer MAY return either property with either strategy.

When it uses page navigation, an integration layer MUST number the first page 1, not 0.

Page navigation is easy to use. A presentation layer can send a user straight to page 7, and the user sees 'page 7 of 20'. The cost is that the integration layer has to count or skip over items, which becomes slow on a large collection. It also becomes less reliable over time: if items are added or removed while a user is browsing, the numbers move, and the user can see an item twice or miss it.

With cursor navigation, the integration layer does not have to count. It remembers where the previous response ended and continues from there. This is fast on a large collection, and it does not shift when items are added or removed. The cost is that a presentation layer cannot jump to an arbitrary page, because it has no number to jump to. For that reason a collection that uses cursor navigation usually has no last page: the integration layer does not know which page is last. An integration layer that does know MAY set last. A presentation layer can then follow last and walk back through the collection with prev.

A cursor is a token that only the integration layer understands. A presentation layer MUST pass a cursor back unchanged, and MUST NOT try to read it or build one. A cursor MUST only be used with the collection and the query it came from. A presentation layer MUST NOT reuse a cursor for another collection or another query.

An integration layer MUST respond with a 400 status code if page has a value of the wrong kind for the strategy that the collection uses: a number where the collection expects a cursor, or a cursor where it expects a number. It MUST NOT treat a page value it does not understand as page 1, because a presentation layer would then be shown the wrong page without knowing it.

Example​

The examples below show the two ways in which a presentation layer can navigate through a collection.

Example of the response body with page navigation:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/objects?page=3",
"type": "Page",
"name": "Heritage objects: page 3",
"items": [
{
"id": "https://example.org/v1/entities/1234",
"type": "HeritageObject",
"name": "The Night Watch"
// Other properties...
}
// Other items...
],
"prev": {
"id": "https://example.org/v1/collections/objects?page=2",
"type": "Page"
},
"next": {
"id": "https://example.org/v1/collections/objects?page=4",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections/objects",
"type": "Collection",
"name": "Heritage objects",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/objects?page=1",
"type": "Page"
},
"last": {
"id": "https://example.org/v1/collections/objects?page=20",
"type": "Page"
}
}
}

The response indicates that this page contains items (items), is related to a previous page (prev) and a next page (next) and its items are part of a collection (partOf). Because the integration layer knows which page is last, partOf points to it.

Example of the response body with cursor navigation:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/objects?page=eyJpZCI6MTIzfQ",
"type": "Page",
"name": "Heritage objects",
"items": [
{
"id": "https://example.org/v1/entities/1234",
"type": "HeritageObject",
"name": "The Night Watch"
// Other properties...
}
// Other items...
],
"prev": {
"id": "https://example.org/v1/collections/objects?page=eyJpZCI6MTAwfQ",
"type": "Page"
},
"next": {
"id": "https://example.org/v1/collections/objects?page=eyJpZCI6MTQ1fQ",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections/objects",
"type": "Collection",
"name": "Heritage objects",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/objects?page=eyJpZCI6MX0",
"type": "Page"
}
}
}

This response has the same properties as the previous one, with two differences. The page is identified by a cursor instead of a number. The partOf property has no last, because the integration layer does not know which page is last. A presentation layer can walk through the collection with next, but it cannot jump to the end.

Resource identifiers​

A resource is identified by a URI. The requirements below apply to those URIs. They follow the REST API Design Rules, which are the source and give the wider context.

  1. A resource MUST be named with a noun, not a verb: a resource is a thing, not an action. For example: https://example.org/v1/collections/works-of-art, not https://example.org/v1/collections/search-works-of-art.
  2. A collection resource MUST be named with a plural noun. For example: https://example.org/v1/collections/works-of-art, not https://example.org/v1/collections/work-of-art.
  3. A URI MUST NOT end with a trailing slash. For example: https://example.org/v1/collections/works-of-art, not https://example.org/v1/collections/works-of-art/.
  4. A path segment MUST contain only lowercase letters, digits and hyphens. For example: https://example.org/v1/collections/works-of-art, not https://example.org/v1/collections/Works-of-Art or https://example.org/v1/collections/works_of_art.
  5. A query key MUST use camel case. For example: https://example.org/v1/collections/works-of-art?orderBy=dateCreated, not https://example.org/v1/collections/works-of-art?order-by=date-created.

Regardless of these requirements, a URI MUST still be treated as an opaque string by a presentation layer. A presentation layer MUST NOT rely on the pattern of a URI. The patterns help developers understand the API, not software to interact with it.