Traverson – ein Hypermedia API Client für Node.js und den Browser

Keine Kommentare

Bei vielen REST APIs sind die einzelnen Ressourcen untereinander verlinkt. Ein Client kann die Links benutzen, um von Ressource zu Ressource zu navigieren. Der Client muss nur die URI der Wurzel-Ressource kennen, von der aus alle anderen Ressourcen über Links erreichbar sind. Dies verringert die Kopplung zwischen dem API-Anbieter und den Clients. Diese Prinzip wird gemeinhin mit HATEOAS bezeichnet – Hypertext As The Engine Of Application State. REST APIs mit diesem Merkmal werden in letzter Zeit auch öfter als Hypermedia APIs bezeichnet. Wenn es nach Mr REST geht, ist dieses allerdings ohnehin ein Muss für jede REST API.

Traverson ist ein JavaScript-Modul für Node.js und für den Browser, welches das Arbeiten mit solchen APIs stark vereinfacht.

Die von GitHub zur Verfügung gestellte API (https://api.github.com/) ist ein Beispiel für das HATEOAS-Muster. Eine Reise von Ressource zu Ressource könnte mit dieser API folgendermaßen aussehen.

Wir beginngen mit der Wurzel-URI, https://api.github.com/, die folgendes Dokument zurückliefert:

{
  "current_user_url": "https://api.github.com/user",
  "gists_url": "https://api.github.com/gists{/gist_id}",
  "repository_url": "https://api.github.com/repos/{owner}/{repo}",
  ...
}

Dann folgen wir dem Link "repository_url",
mit den Parametern { owner: "basti1302", repo: "traverson" },
und erreichen so die URI https://api.github.com/repos/basti1302/traverson

{
  "id": 13181035,
  "name": "traverson",
  "full_name": "basti1302/traverson",
  "owner": {
    "login": "basti1302",
    ...
  },
  "url": "https://api.github.com/repos/basti1302/traverson",
  "commits_url": "https://api.github.com/repos/basti1302/traverson/commits{/sha}",
  "issues_url": "https://api.github.com/repos/basti1302/traverson/issues{/number}",
  ...
}

Dann folgen wir dem Link "commits_url",
mit dem Parameter { sha: 5c82c74583ee67eae727466179dd66c91592dd4a },
und erreichen so die URI https://api.github.com/repos/basti1302/traverson/commits/5c82c74583ee67eae727466179dd66c91592dd4a

{
  "sha": "5c82c74583ee67eae727466179dd66c91592dd4a",
  "commit": {
    "author": {
      "name": "Bastian Krol",
      ...
    },
    "committer": {
      "name": "Bastian Krol",
      "date": "2013-10-25T11:41:09Z",
      ...
    },
    "message": "test hal support against @mikekelly's haltalk server"
    ...
  },
  "url": "https://api.github.com/repos/basti1302/traverson/commits/5c82c74583ee67eae727466179dd66c91592dd4a",
  "comments_url": "https://api.github.com/repos/basti1302/traverson/commits/5c82c74583ee67eae727466179dd66c91592dd4a/comments",
  ...
}

Zum Schluß folgen wir dem Link "comments_url",
und erreichen so schließlich die URI
https://api.github.com/repos/basti1302/traverson/commits/5c82c74583ee67eae727466179dd66c91592dd4a/comments

[
  {
    "id": 4432531,
    "user": {
      "login": "basti1302",
      ...
    },
    "created_at": "2013-10-25T22:50:59Z",
    "body": "This commit is a commit is a commit.",
    ...
  }
]

Wir sind also drei aufeinanderfolgenden Links gefolgt, haben uns so von Ressource zu Ressource gehangelt, um am Ende zu der Ressource zu gelangen, die uns eigentlich interessiert (die Commit-Kommentare in diesem etwas hanebüchenen Beispiel).

Traverson

Es gibt eine ganze Reihe von Bibliotheken, die den Umgang mit REST APIs vereinfachen. Die meisten davon erleichtern das Arbeiten mit einer Ressource oder einem Request. Es gibt allerdings bisher wenig Unterstützung bei dem oben skizzierten, typischen Muster, bei dem man mehreren Links folgt – zumindest nicht in der JavaScript-Welt. Hier kommt Traverson ins Spiel. Traverson macht aus dem Folgen einer Sequenz von Links einen einzelnen Methodenaufruf.

Anhand der GitHub API können wir uns anschauen, wie das in der Praxis aussieht. Wenn man den Links z. B. mit dem request-Modul für Node.js folgen wollte, könnte man folgenden Code schreiben:

var request = require('request')
var rootUri = 'https://api.github.com/'
 
function nextUri(response, link) {
  var resource = JSON.parse(response.body)
  return resource[link]
}
 
request.get(rootUri, function(err, response) {
  if (err) { console.log(err); return; }
  var uri = nextUri(response, 'repository_url')
  uri = uri.replace(/{owner}/, 'basti1302')
  uri = uri.replace(/{repo}/, 'traverson')
  request.get(uri, function(err, response) {
    if (err) { console.log(err); return; }
    uri = nextUri(response, 'commits_url')
    uri = uri.replace(/{\/sha}/,
        '/5c82c74583ee67eae727466179dd66c91592dd4a')
    request.get(uri, function(err, response) {
      if (err) { console.log(err); return; }
      uri = nextUri(response, 'comments_url')
      request.get(uri, function(err, response) {
        if (err) { console.log(err); return; }
        var resource = JSON.parse(response.body)
        console.log(resource)
      })
    })
  })
})

Eine hübsche Callback-Pyramide, und außerdem repetetiv. Mit Traverson sieht das grundlegende Muster zum Folgen einer Kette von Links bis zur Ziel-Ressource so aus:

var api = require('traverson').json.from('https://api.github.com/')
api.newRequest()
  .follow('repository_url', 'commits_url', 'comments_url')
  .getResource(function(err, resource) {
    // do something with the resource...
  })

Und ein komplettes, funktionsfähiges Beispiel mit der GitHub API sieht so aus:

var api = require('traverson').json.from('https://api.github.com/')
api.newRequest()
  .follow('repository_url', 'commits_url', 'comments_url')
  .withTemplateParameters({
    owner: 'basti1302',
    repo: 'traverson',
    sha: '5c82c74583ee67eae727466179dd66c91592dd4a'
  }).getResource(function(err, resource) {
    if (err) { console.log(err); return; }
    console.log(resource)
  })

Das ist deutlich kürzer als die erste Version und es wird nur ein Callback benötigt, der erst aufgerufen wird, wenn die Ziel-Ressource erreicht ist.

Was genau passiert in diesem Code-Beispiel? Traverson ruft die Wurzel-URI ab, die über die Methode from() vorgegeben wird. Von da aus sucht es sich selbstständig seinen Weg durch die API, in dem es den Links folgt, die der Methode follow übergeben wurden. Zum Schluss übergibt Traverson die letzte Ressource in dieser Kette dem Callback. Tatsächlich passiert das alles erst, wenn die Methode getResource aufgerufen wird, alle anderen Methoden sind nur vorbereitende Konfiguration.

Traverson Features

URI Templates

Abgesehen vom Folgen von Links bietet Traverson noch einige weitere Features. Eins davon haben wir im obigen Beispiel bereits benutzt, das Auflösen von URI-Templates.

Oft ist der Verweis zur nächsten Ressource keine konkrete URI sondern ein RFC 6570 URI-Template. Wenn Traverson ein solches Template erkennt und wenn Werte für die Template-Parameter über die Methode withTemplateParameters zur Verfügung gestellt wurden, werden die Platzhalter im Template automatisch ersetzt.

Abgesehen von URI-Templates bietet Traverson noch eine Reihe anderer nützlicher Features:

  • basic authentication,
  • OAuth,
  • beliebige HTTP Header,
  • JSONPath,
  • HAL (hypertext application language)

Im Folgenden werden einige davon im Detail beschrieben.

JSONPath

Bis jetzt haben wir die Links, denen Traverson folgen soll, dadurch bestimmt, dass wir der Methode follow eine Reihe von Property-Namen übergeben haben. Traverson sucht in jeder Ressource jeweils nach dem Property-Namen, um herauszufinden, welche URI als nächstes abgerufen werden muss.
Hier stellt sich die Frage, was passiert, wenn der Link kein direktes Property der Ressource ist. Wenn zum Beispiel die Ressource so aussieht:

{
  "deeply": {
    "nested": {
      "link: "http://api.io/congrats/you/have/found/me"
    }
  }
}

Um den Link zu http://api.io/congrats/you/have/found/me zu folgen, kann man der Methode follow auch JSONPath-Ausdrücke übergeben:

api.newRequest()
  .follow('$.deeply.nested.link')
  .getResource(function(error, document) {
    ...
  })

Natürlich lassen sich JSONPath-Ausdrücke und normale Property-Namen in einem Aufruf von follow mischen.

Basic Authentication

Unter der Haube benutzt Traverson das zuvor erwähnte request-Modul, zumindest unter Node.js. (In Browser wird request durch einen auf superagent basierenden Shim ersetzt.) Daher können alle Features von request, wie Basic Authentication, OAuth und das Senden von beliebigen HTTP-Headern auch mit Traverson benutzt werden. Basic Authentication mit der GitHub API sähe so aus:

var api = require('traverson').json.from('https://api.github.com/'),
    ghUser = 'basti1302',
    ghPass = '...'
 
api.newRequest()
  .follow(...)
  .withRequestOptions({
    auth: {
      user: ghUser,
      pass: ghPass,
      sendImmediately: true
    }
  }).getResource(function(err, resource) {
    ...
  })

HAL – Hypertext Application Language

HAL, die Hypertext Application Language, ist ein einfaches Format, das es ermöglicht, Ressourcen in einer API auf einfache und konsistente Weise untereinander zu verlinken. Es gibt eine formale Spezifikation für dieses Format und den Media Type application/hal+json. HAL lässt sich nicht darüber aus, wie die eigentlichen Inhalte in JSON dargestellt werden sollen. Es beschreibt ausschließlich, wie und wo die Hyperlinks präsentiert werden. Dies erleichtert die Implementierung von Clients, da immer klar ist, wo im Dokument die Links zu finden sind, auch, wenn ein Client mit verschiedenen APIs redet. Auf http://haltalk.herokuapp.com gibt es ein Live-HAL-Demo. Wenn man dort zum Beispiel von der Wurzel-Ressource über die Links ht:users, ht:user und ht:posts navigiert, erreicht man die Liste der Posts für einen Benutzer.

Das geht auch mit Traverson. Dazu benutzt man require('traverson').jsonHal statt require('traverson').json.

var api = require('traverson').jsonHal.from('http://haltalk.herokuapp.com/')
 
api.newRequest()
  .withRequestOptions({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    }
  }).follow('ht:users', 'ht:user', 'ht:posts', 'ht:post')                                                                      
  .withTemplateParameters({name: 'mike'})                                   
  .getResource(function(error, document) {
     // document will represent the list of posts for user mike
  })

Möchte man hingegen einen neuen Eintrag posten, sähe das so aus:

var body = { content: 'Hello there!' }
api.newRequest()
  .withRequestOptions({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    auth: {
      user: 'traverson',
      pass: '...', // traverson's password would go here :-)
      sendImmediately: true
    }
  }).follow('ht:me', 'ht:posts')
  .withTemplateParameters({ name: 'traverson' })
  .post(body, function(error, respons) {
    if (err) { console.log(err); return; }
    if (response.statusCode === 201) {
      console.log('unexpected http status: ' + response.statusCode)
    }
  })

In beide Beispielen mussten wir die Header Accept und Content-Type setzen, damit der Haltalk-Server unseren Request korrekt bearbeitet. Da Haltalk das Posten nur angemeldeten Benutzern erlaubt, mussten wir im zweiten Beispiel auch Benutzer und Passwort mitliefern.

Im Browser

In server-seitigem JavaScript ist das Ansprechen von REST-APIs ein häufig auftretender Anwendungsfall. Deshalb ist Traverson zuerst als Node.js-Modul konzipiert worden. Allerdings kann Traverson sich im Browser als genauso nützlich erweisen, wenn man über AJAX mit REST-APIs arbeitet. Daher funktioniert Traverson dank browserify mittlerweile auch im Browser. Der Browser-Build ist eine einzelne Datei mit einer UMD die entweder per script-Tag eingebunden werden kann oder über einen AMD-Loader wie RequireJS geladen werden kann.

Die Benutzung von Traverson im Browser unterscheidet sich nicht von der Benutzung in Node.js:

<html>
  <head>
    <meta charset="utf-8">
    <title>Traverson in the Browser</title>
    <script src="http://code.jquery.com/jquery-2.0.2.min.js"></script>
    <script src="traverson.min.js"></script>
  </head>
  <body>
 
    <div id="response"/>
 
    <script>
      var api = traverson.json.from('https://api.github.com/')
      api.newRequest()
        .follow('repository_url', 'commits_url', 'comments_url')
        .withTemplateParameters({
          owner: 'basti1302',
          repo: 'traverson',
          sha: '5c82c74583ee67eae727466179dd66c91592dd4a'
        }).getResource(function(err, resource) {
        if (err) {
          $('#response').html(JSON.stringify(err))
          return
        }
        $('#response').html('Hooray! The commit comment is <br/>"' +
            resource[0].body + '"')
      })
    </script>
  </body>
</html>

Bastian Krol entwickelt seit über 15 Jahren Enterprise-Systeme und Open-Source-Software. Seine Schwerpunkte sind Java, JavaScript, Node.js und Hypermedia-APIs.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.