---
title: Build a RESTful API
url: https://docs.triply.cc/tutorials/building-a-restful-api/
description: A tutorial that turns a SPARQL query into a RESTful API endpoint on TriplyDB, from writing
  the iterator to serving the results.
---

# 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](https://docs.triply.cc/concepts/sparql-basics/),
[construct queries](https://docs.triply.cc/concepts/sparql-construct-and-federation/) and
[JSON-LD framing](https://docs.triply.cc/tutorials/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](https://docs.triply.cc/how-to/create-an-api-token/). Create a dataset named `pokemon` in your account,
click **Import from URL** on its Graphs page, and import:

```iri
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](https://docs.triply.cc/how-to/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`:

```sparql
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:

```sparql
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:

```sparql
bind($hoennNumber as ?hoennNumber1)
optional { $this vocab:hoennNumber ?hoennNumber2. }
filter(!bound(?hoennNumber1) || ?hoennNumber1 = ?hoennNumber2)
```

Each line does one thing:

1. The Bind clause makes sure that `?hoennNumber1` is only bound if `hoennNumber` is specified
   through a query parameter.
2. The Optional clause matches the optional property, if it is present.
3. 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:

```sparql
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:

```sparql
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](#write-the-iterator) to
[the basic template in the Generator](#the-basic-construct-query):

```sparql
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:

```sparql
  $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:

```sparql
  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](#full-example) as its Sub-Select:

```sparql
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](https://docs.triply.cc/how-to/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](https://docs.triply.cc/tutorials/json-ld-framing/); this section only gives the
frame for Pokémon.

Save the following as `frame.json`:

```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 for `vocab:Pokémon`.
- **`@base` and `@vocab`** shorten the IRIs of the instances and of the dataset's own vocabulary,
  so `vocab:colour` comes back as `colour`.
- **The prefix declarations** (`rdfs`, `sdo`, `xsd`) shorten every other IRI.
- **The key names** (`name`, `audio`, `encodingFormat`, …) replace prefixed IRIs with plain names,
  and `id` and `type` replace the `@id` and `@type` keys.
- **`baseHP` and `contentUrl`** declare their datatypes, so they come back as plain strings rather
  than as objects with `@type` and `@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:

```sh
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:

```json
{
  "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](https://docs.triply.cc/tutorials/json-ld-framing/) — how the frame works, step by step
- [SPARQL: graph patterns](https://docs.triply.cc/concepts/sparql-graph-patterns/) — `optional`, `filter` and
  property paths, all used above
- [Work with saved queries](https://docs.triply.cc/how-to/saved-queries/) — the queries this API is built from
- [Paginate SPARQL results](https://docs.triply.cc/how-to/sparql-pagination/) — walking through every page of the
  unfiltered result
