Build a RESTful API¶
Applications want a REST API: a path, some query parameters, and JSON back. Your data is a graph. This tutorial connects the two using nothing but saved SPARQL queries and a JSON-LD frame — no service layer, and no code.
The example throughout is an API path for Pokémon, built on the public Pokémon dataset that Triply publishes on triplydb.com. It assumes SPARQL: the basics, construct queries and JSON-LD framing.
The design has two queries. The iterator decides which things the API returns. The generator decides what is returned about each of them.
Before you start¶
You need an account on a TriplyDB instance and an
API token. Create a dataset named pokemon in your account,
click Import from URL on its Graphs page, and import:
https://api.triplydb.com/datasets/academy/pokemon/download.trig.gz
When the import finishes, the dataset holds two graphs with 28,773 statements between them. See Add data if any of that is unfamiliar.
Write the Iterator¶
The iterator is a SPARQL Select query that returns a sequence of bindings that adheres to the query parameters supported for the REST API path.
The REST API path for Pokémon returns bindings for individual Pokémon, and has the following query parameters:
- The name of the Pokémon (API variable
$name) - Its number in the regional Hoenn Pokédex (API variable
$hoennNumber)
Configuration in case no query parameter is specified¶
The first part of the query string returns the sequence of bindings in case no query parameter is
specified. It names the class that corresponds to the REST API path, vocab:Pokémon:
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select $this {
$this
a vocab:Pokémon.
}
Configuration for query parameters that map onto required properties¶
For every query parameter, determine whether it maps onto a property that is required or optional.
Query parameter name maps onto property rdfs:label, which every Pokémon has. For each required
property, add a Basic Graph Pattern:
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select $this {
$this
a vocab:Pokémon;
rdfs:label $name.
}
Configuration for query parameters that map onto optional properties¶
Query parameter hoennNumber maps onto property vocab:hoennNumber, which 107 of the 493 Pokémon
do not have. A basic graph pattern would drop every Pokémon without one, so an optional property
needs three lines instead:
bind($hoennNumber as ?hoennNumber1)
optional { $this vocab:hoennNumber ?hoennNumber2. }
filter(!bound(?hoennNumber1) || ?hoennNumber1 = ?hoennNumber2)
Each line does one thing:
- The Bind clause makes sure that
?hoennNumber1is only bound ifhoennNumberis specified through a query parameter. - The Optional clause matches the optional property, if it is present.
- The Filter clause lets the query succeed if either the query parameter was not set, or it was
set to the value of the current binding for
$this.
For another optional parameter, repeat the three lines with its own API variable and property.
Full example¶
The following query is the complete Iterator, with both query parameters:
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
select $this {
$this
a vocab:Pokémon;
rdfs:label $name.
bind($hoennNumber as ?hoennNumber1)
optional { $this vocab:hoennNumber ?hoennNumber2. }
filter(!bound(?hoennNumber1) || ?hoennNumber1 = ?hoennNumber2)
}
Write the Generator¶
The Generator is a SPARQL Construct query that returns one record for each binding of the $this
variable in the Iterator. Since we are working with linked data graphs, there is no real notion of
a 'record'. This requires us to configure which data items we want to include, and which we want
to exclude.
The basic Construct query¶
Since the Generator is a Construct query, start with the following basic query, which returns triples from the graph:
construct {
$this ?p1 ?o1.
} where {
$this ?p1 ?o1.
}
It uses the same variable name $this as the Iterator, but that is merely a naming convention.
The next step connects the two.
Integrate the Iterator into the Generator¶
In SPARQL, any Select query can be placed inside another query as a Sub-Select clause. That
connects the bindings for $this that come out of the Iterator to
the basic template in the Generator:
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
construct {
$this ?p1 ?o1.
} where {
{
select $this {
$this
a vocab:Pokémon;
rdfs:label $name.
}
}
$this ?p1 ?o1.
}
With a value for name, this returns every triple that describes the Pokémon with that name —
at least 46 of them for every Pokémon, from base statistics to links into other datasets.
Choose the properties to return¶
An API returns what its consumers need, not everything the graph holds. A whitelist of properties,
written with the operator in, keeps the rest out:
$this ?p1 ?o1.
filter(?p1 in (rdf:type,
rdfs:label,
vocab:colour,
vocab:baseHP,
vocab:hoennNumber,
sdo:audio))
Extend the Generator with nested information¶
So far the Generator only returns the triples that directly describe $this. Some relevant
information sits further away. Each Pokémon links to its sound recordings with sdo:audio, and
the address and format of each recording are on the recording, two hops away from $this.
To include such nested information, add another Basic Triple Pattern. Since only some properties lead to nested information, enclose the deeper hop inside an Optional clause, together with a whitelist of the properties to follow:
optional {
?o1 ?p2 ?o2.
filter(?p1 in (sdo:audio))
}
Full example¶
The following query is the complete Generator, with the full Iterator from the previous full example as its Sub-Select:
prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix sdo: <http://schema.org/>
prefix vocab: <https://triplydb.com/academy/pokemon/vocab/>
construct {
$this ?p1 ?o1.
?o1 ?p2 ?o2.
} where {
{
select $this {
$this
a vocab:Pokémon;
rdfs:label $name.
bind($hoennNumber as ?hoennNumber1)
optional { $this vocab:hoennNumber ?hoennNumber2. }
filter(!bound(?hoennNumber1) || ?hoennNumber1 = ?hoennNumber2)
}
}
$this ?p1 ?o1.
filter(?p1 in (rdf:type,
rdfs:label,
vocab:colour,
vocab:baseHP,
vocab:hoennNumber,
sdo:audio))
optional {
?o1 ?p2 ?o2.
filter(?p1 in (sdo:audio))
}
}
Save the Generator¶
Open the SPARQL IDE of your pokemon dataset, paste the full Generator in, and save it as
pokemon. On the saved query's page, click Add variable twice, to declare name and
hoennNumber as its API variables. The saved query now has an API URL of the form
https://api.triplydb.com/queries/ACCOUNT/pokemon/run, where ACCOUNT is your account name. See
Work with saved queries for the save itself.
Shape the JSON with a frame¶
From a content perspective, the Generator already functions like a REST API path: it takes zero or more query parameters, and returns the information that conforms to its configuration. It returns JSON-LD, but a JSON-LD graph has no root and no fixed shape, so the body is correct but messy. A JSON-LD frame turns it into a predictable tree. How a frame works, and how to try one out on the query page, is the subject of JSON-LD framing; this section only gives the frame for Pokémon.
Save the following as frame.json:
{
"@context": {
"@base": "https://triplydb.com/academy/pokemon/",
"@version": 1.1,
"@vocab": "https://triplydb.com/academy/pokemon/vocab/",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"sdo": "http://schema.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"id": "@id",
"type": "@type",
"name": "rdfs:label",
"audio": "sdo:audio",
"baseHP": { "@id": "baseHP", "@type": "xsd:integer" },
"contentUrl": { "@id": "sdo:contentUrl", "@type": "xsd:anyURI" },
"encodingFormat": "sdo:encodingFormat",
"AudioObject": "sdo:AudioObject"
},
"type": "Pokémon"
}
Each part removes one kind of clutter from the response:
"type": "Pokémon"makes every top-level object a Pokémon. It resolves against@vocab, so it stands forvocab:Pokémon.@baseand@vocabshorten the IRIs of the instances and of the dataset's own vocabulary, sovocab:colourcomes back ascolour.- The prefix declarations (
rdfs,sdo,xsd) shorten every other IRI. - The key names (
name,audio,encodingFormat, …) replace prefixed IRIs with plain names, andidandtypereplace the@idand@typekeys. baseHPandcontentUrldeclare their datatypes, so they come back as plain strings rather than as objects with@typeand@value.
Call the API¶
Send the frame to the saved query, with the query parameter in the URL. Replace ACCOUNT with your
account name and TOKEN with your API token:
curl -X POST 'https://api.triplydb.com/queries/ACCOUNT/pokemon/run?name=Pikachu' \
-H 'Accept: application/ld+json;profile=http://www.w3.org/ns/json-ld#framed' \
-H 'Content-type: application/json' \
-H 'Authorization: Bearer TOKEN' \
-d @frame.json
The response holds the frame's @context, followed by Pikachu:
{
"id": "id/pokemon/pikachu",
"type": "Pokémon",
"audio": [
{
"id": ".well-known/genid/28ae2a933c01ba95bb18f7933a1ad931",
"type": "AudioObject",
"contentUrl": "https://api.triplydb.com/datasets/academy/pokemon/assets/download?fileName=pikachu.ogg",
"encodingFormat": "audio/ogg",
"name": "Audio of Pikachu in Ogg format."
},
{
"id": ".well-known/genid/3516db27c9964b304b805c1220c04351",
"type": "AudioObject",
"contentUrl": "https://api.triplydb.com/datasets/academy/pokemon/assets/download?fileName=pikachu.mp3",
"encodingFormat": "audio/mpeg",
"name": "Audio of Pikachu in MP3 format."
}
],
"name": "Pikachu",
"baseHP": "35",
"colour": "yellow",
"hoennNumber": "156"
}
Replace ?name=Pikachu with ?hoennNumber=39 and the same request returns Abra. Leave the
parameters out and it returns every Pokémon, a page at a time.
The same account also publishes an OpenAPI specification describing all of its saved queries:
request https://api.triplydb.com/queries/ACCOUNT/ with the API token and the header
Accept: text/vnd.yaml, and load the result into any client that reads OpenAPI.
What you now have¶
A REST API backed entirely by saved SPARQL queries: parameters that behave like query parameters, JSON that looks hand-designed, and an OpenAPI specification any client can import. Changing what the API returns is editing a query, not deploying code.
Related¶
- JSON-LD framing — how the frame works, step by step
- SPARQL: graph patterns —
optional,filterand property paths, all used above - Work with saved queries — the queries this API is built from
- Paginate SPARQL results — walking through every page of the unfiltered result