Pragmatische Küchentricks für RESTful HAL APIs

Seite 3: Relationen als Einstiegspunkte für die Dokumentation

Inhaltsverzeichnis

Die Dokumentation der API ist ebenso wichtig wie die API selbst, gerade dann, wenn die API wie im vorliegenden Fall in einem komplexen Konsumenten-Ökosystem genutzt wird. Häufig kommen dazu Definitionssprachen wie Swagger/OpenAPI, RAML oder Blueprint zum Einsatz. Sie werden damit über die dazugehörige Software-Tools-Dokumentation für Menschen generiert. Gute Beispiele dafür sind Swagger UI und Redoc – basierend auf der OpenAPI-Spezifikation. Es ist erwähnenswert, dass sich viele dieser Dokumentations-Tools nahtlos in die API integrieren lassen, zum Beispiel durch Client-Rendering.

Anfänglich hat das Projektteam auf Swagger/OpenAPI gesetzt und das dort mögliche Tagging im Quellcode von Endpunkten als Bindeglied zu den Relationen verwendet. Obwohl das Tool die Erstellung und Pflege der Dokumentation vereinfachte, hat es durch den Fokus auf URIs und Endpunkte – statt Links – den Hypermedia-Vorteil von REST kompromittiert. Im Laufe der Zeit zeigte sich, wie API-Konsumenten die angebotenen Relationen ignorierten und Annahmen trafen, die auf dem Lesen von URIs und dem daraus resultierenden "vermeintlichen Verständnis" basierten. In jüngerer Zeit hat man sich deshalb dafür entschieden, URIs und interne IDs in der Dokumentation zu verstecken und Swagger UI durch den HAL Browser, ein generisches Tool zur Exploration HAL-basierter APIs, zu ersetzen.

Für die API wurde ein angepasster Dokumentationsansatz gewählt. Zum einen wurden Profile-Ressourcen (siehe ALPS JsonSchema) für alle exponierten Modelle (Repräsentation) eingeführt und zum anderen Relations-Ressourcen erstellt, über die es Details wie Endpunkte, Mediatypes, Status-Codes und Extra-Markdown für Architectural Decisions (ADRs) zu erfahren gab. Diese beiden Ressourcen-Typen hat das Unternehmen miteinander verknüpft und in einem Relationen-Register als Start-Ressource der API bereitgestellt.

<h1>Documentation for rel='planes'</h1>
<div id="deprecation">
    <h2>Deprecation</h2>false
</div>
<div id="description">
    <h2>Description</h2>
    <h3>PLANES (Collection)</h3>
    <p>Lorem Ipsum Lorem Ipsum Lorem Ipsum …</p>
</div>
<div id="endpoints">
    <h2>Endpoints</h2><!-- status codes omitted --> 
    <ul>
        <li>GET http://example.com/planes{?makeName,modelName,modelYear,...,sort,page,size}</li>
        <li>POST http://example.com/planes</li>
    </ul>
</div>
<div id="profiles">
    <h2>Profile (Schema)</h2>
    <ul>
        <li><a href="http://example.com/profile/PlaneResource">PlaneResource</a></li>
    </ul>
</div>

Listing 7: Relationen-Ressource (HTML) für 'planes' unter /rels/planes

{
  "type": "object",
  "id": "urn:jsonschema:com:example:resource:PlaneResource",
  "description": "Profile for com.example.resource.PlaneResource",
  "properties": {
    "links": {
      "type": "array",
      "items": {
        "type": "object",
        "$ref": "urn:jsonschema:org:springframework:hateoas:Link"
      }
    },
    "urn": {
      "type": "string",
      "readonly": true
    },
    "country": {
      "type": "string",
      "code": [
        "GER",
        "USA",
        "CHL"
      ]
    },
    "makeName": {
      "type": "object",
      "$ref": "urn:jsonschema:com:example:NameValue"
    },
    "modelName": {
      "type": "object",
      "$ref": "urn:jsonschema:com:example:NameValue"
    },
    "trimLevel": {
      "type": "object",
      "$ref": "urn:jsonschema:com:example:NameValue"
    }
  }
}

Listing 8: Profile-Ressource (ALPS) für 'PlaneResource' unter /profile/PlaneResource

{
    "_links": {
        "self": {
            "href": "http://example.com/planes/123"
        },
        "ex:planes": {
            "href": "http://example.com/planes/"
        },
        "profile": {
            "href": "http://example.com/profile/PlaneResource",
        },
        "curies": // …
    }
}

Listing 9: Anwendungen in der Ressource 'Plane#123' unter /planes/123

Dieses Konstrukt funktioniert wunderbar mit dem HAL-Explorer, wie in Abbildung 1 zu sehen ist. Jede Link-Relation wird über das docs Icon zu ihrer Dokumentation verlinkt, dabei wird der Curie-Kennzeichner automatisch in die entsprechende URI übersetzt.

HAL-Explorer (früher HAL-Browser) mit beispielhafter Relations-Dokumentation (Abb. 1)

Das Unternehmen des Autors hat auch mehrere Konsumenten des Backends entwickelt. Der vermutlich komplexeste Client war ein Administrations-Tool. Es war dazu gedacht, die Daten der ETL-Prozesse noch mit weiteren Informationen anzureichern oder zu korrigieren. Die Anzahl der Nutzer war naturgemäß klein – die Anforderungen dafür höchst dynamisch – sie wuchsen nahezu organisch mit den jeweils neu hinzukommenden Daten.

Eine Annehmlichkeit beim Einsatz gereifter Technologien ist die große Verfügbarkeit von ebenfalls ausgereiften Tools und Open-Source-Bibliotheken zur Implementierung der Clients. Hauptsächlich eingesetzt wurde Traverson, eine Hypermedia-API beziehungsweise ein HATEOAS-Client für Node.js, um über die HAL-Links zu navigieren. Traverson bietet eine Fülle aneinanderreihbare Methoden, um von einer Start-Ressource ausgehend nur über Relationen durch die API zu navigieren. Ein typischer Request sieht daher wie folgt aus:

const childList = await traverson
    .from("http://example.com")
    .withRequestOptions(apiRequestHeaders)
    .jsonHal()
    .newRequest()
    .withTemplateParameters({ parentId })
    .follow("ex:planes", "ex:models")
    .getResource()
    .result;

Listing 10: Implementierung im Client mit Traverson in Node.js

Unter der Haube holt sich Traverson die Start-Ressource und durchsucht diese nach der URL für die Relation ex:planes. Sofern die Relation wie im Beispiel templated ist, werden die parentId eingesetzt und die finale URI zusammengebaut. Die zurückgelieferte Ressource des Requests wird dann nach ex:models analysiert. Ein weiterer GET-Request folgt, um das Ergebnis zu holen. Die Verwendung von Labeln und Verkettung von Aufrufen hat das Finden und Lesen von Ressourcen stark vereinfacht und die Kohäsion von API und Konsumenten auf das Notwendigste reduziert. Die eindeutigen Relationsbezeichner machten den Client unabhängig von der eingesetzten Umgebung und vereinfachten die Auffindbarkeit in der Codebasis, was vor allem bei Änderungen im Backend schnelles Feedback möglich machte.