W-JAX Blog

W-JAX - Die Konferenz für Java, Architektur- und Softwareinnovation
5. - 9. November 2018 I München
5
Mrz

RESTful APIs richtig gemacht – Anleitung für bessere REST-Schnittstellen

RESTful APIs richtig gemacht!

Quelle: Shutterstock

Wer schon einmal eine Domäne mit Microservices aufgebaut hat, wird es bereits wissen: APIs für die Service-zu-Service-Kommunikation sind von zentraler Bedeutung. Da jedes Team seinen eigenen Stil hat und Schnittstellen jeweils anders implementiert, kommt es über kurz oder lang zu einem Wildwuchs von verschiedenen Ansätzen. Gleich zu Projektbeginn einen Leitfaden mit Richtlinien und Beispielen zu definieren, hilft, einheitliche und möglichst selbsterklärende APIs zu gewährleisten.

von Thomas Spiegl und Manfred Geiler
REST mit JSON ist der heute am weitesten verbreitete Ansatz für neue Programmierschnittstellen. Unzählige Bücher, Vorträge, Blogs und andere Quellen im Internet beschäftigen sich mit dem Thema. Trotzdem scheint es große Auffassungsunterschiede in der Entwicklergemeinde zu geben, wie Webschnittstellen auszusehen haben. Die JSON-API-Spezifikation [1] legt genau fest, wie ein RESTful API basierend auf einem einheitlichen Standard implementiert werden sollte.
Zunächst aber noch ein paar Worte zu APIs. Bei der Definition eines API sollte sich der Entwickler genug Zeit nehmen, denn gut entworfene APIs führen automatisch zu besseren Produkten. Es empfiehlt sich, zu Beginn eines Projekts Richtlinien zu entwickeln, die die zentralen Anforderungen an ein API festhalten. Mithilfe von Schlüsselwörtern laut RFC 2119 [2] lässt sich festlegen, wie wichtig (oder zwingend) eine einzelne Anforderung ist. Themen für einen Richtlinienkatalog sind beispielsweise API-Namensgebung, HTTP Header und Operationen, Anfrageparameter oder Dokumentstruktur.
Richtlinien für den API-Entwurf helfen, ein gemeinsames Verständnis zu entwickeln. Grundsätzlich lässt sich sagen, dass Services, die einheitliche Standards verfolgen, leichter zu verstehen und einfacher zu integrieren sind. Daneben lassen sie sich effizienter umsetzen (gemeinsame Tools), sind stabiler gegenüber Änderungen und einfacher zu warten.

RESTful Styleguide für JSON-APIs

In der JSON-API-Spezifikation [1] finden sich konkrete Vorschläge, wie ein gut entworfenes REST API aussehen kann. Aus unserer Sicht ist die Spezifikation ein guter Einstiegspunkt ins Thema. Für unsere Beispiele begeben wir uns gedanklich in die Welt der Sportvereine. Wir wollen mit unserer gedachten IT-Lösung Teams, Manager und Spieler verwalten.

 


Abb. 1: Datenmodell

 

Das Diagramm (Abb. 1) zeigt die Entität Team und seine Beziehungen. Ein Team wird von einem Manager trainiert. Einem Team werden mehrere Spieler (Player) zugeordnet. Manager und Player sind jeweils vom Typ Person.

 

Manager oder Coach?

Die Begriffe Manager und Head Coach werden im englischen Fußball synonym für den Cheftrainer einer Mannschaft verwendet. Zum Zwecke der Lesbarkeit verwenden wir hier den Begriff Manager.

 

Das API: Mit wem dürfen wir sprechen?

Den Einstieg ins API bietet für unsere Beispielapplikation die zentrale Domain Entity Team. Über einen API-URL-Pfad werden klassische Operationen wie Lesen, Schreiben, Ändern, Löschen oder die Verwaltung von Beziehungen ermöglicht. Der Pfad dient auch der Dokumentation für den API-Anwender und sollte daher klar und einfach zu deuten sein. Als Einstiegspunkt für die Ressource Team bietet sich der Pfad /teams an. Der Pfadname ist in der Mehrzahl angegeben, schließlich sollen ja mehrere Teams gleichzeitig verwaltet werden können.
Wir legen daher im API fest, dass eine Entität über den Ressourcenpfad /{entity-type} verwaltet werden kann. Es handelt sich dabei typischerweise um eine Collection (1 bis n Teams). Als Namenskonvention gilt: ein Entity Type ist (oder endet auf) ein Nomen in der Mehrzahl, enthält nur Kleinbuchstaben, und einzelne Worte werden durch ein Minus getrennt:

 

richtig: /teams, /team-memberships

falsch: /Teams, /team, /teamMemberships, /team-search

 

Datenobjekte abfragen: Welche Mannschaften gibt es eigentlich?

Beginnen wir mit einem einfachen HTTP GET auf den API-Pfad /teams:

GET /teams HTTP/1.1Accept: application/vnd.api+json

Im HTTP Header Accept mit dem Wert application/vnd.api+json wird festgelegt, dass in der Antwort ein JSON-Dokument erwartet wird. Jeder HTTP Request sollte diesen Header setzen. Das zurückgelieferte Dokument enthält ein Array aller Team-Objekte. Eine Response könnte also etwa so aussehen wie in Listing 1.

Listing 1: Array aller „Team“-Objekte


{

   "data": [

       {

           "id": "1",

           "type": "teams",

             "attributes": {  

                 "name": "FC Norden Jugend",  

                 "category": "juniors"  

             },  

             "links": {  

                 "self": "http://example.com/teams/1"  

             }  

         },  

         {  

             "id": "2",  

             "type": "teams",  

             "attributes": {  

                 "name": "FC Essen",  

                 "category:" "masters"  

             },  

             "links": {  

                 "self": "http://example.com/teams/2"  

               }

       }

   ]

}

Die Dokumentstruktur: Ordnung ist das halbe Leben

Zu einem gut strukturierten API gehört auch eine vorgegebene Dokumentstruktur. Sie hilft, wiederverwendbare Werkzeuge zu bauen, auch um das Zusammenspiel von Komponenten und den Daten im User Interface zu erleichtern. Ein API mit einheitlicher Struktur wirkt wie aus einem Guss – auch dann, wenn unterschiedliche Entwicklerteams an seiner Definition arbeiten. Die Struktur eines JSON-Dokuments wird von der JSON API Specification [3] genau festgelegt. Jedes Dokument besitzt immer eines von zwei Elementen: data oder errors. Optional können auch die Elemente meta, jsonapi, links oder included auf oberster Ebene enthalten sein. Das Element data enthält die eigentlichen Daten und kann entweder aus einem einzelnen Resource-Objekt oder aus einem Array von Resource-Objekten bestehen. Zusätzlich können Objektreferenzen oder auch null enthalten sein. Im Dokument aus dem letzten Beispiel wird im data-Element ein Ressource-Array, eine Liste von Datenobjekten vom Typ teams, geliefert. Das Datenobjekt wiederum hat die Elemente id, type, attributes und links. Die Attribute id und type stellen eine Referenz {„id“: „1“, „type“: „teams“} auf eine Entität vom Typ teams dar. Jede Ressource muss diese beiden Attribute besitzen. Datenobjekte bleiben so auch losgelöst vom API noch eindeutig identifizierbar. Der Typ deckt sich mit dem Pfadnamen im API und ist so immer ein Nomen im Plural. Die eigentlichen Daten (also z. B. „name“: „FC Essen“) der Entität werden im Element attributes geführt. Es scheint untypisch, dass Daten von der Objektreferenz getrennt gehalten werden. Im klassischen RDBMS oder in JPA werden Daten und Identifier meist gleichberechtigt in der Entity geführt. Die Trennung ist dennoch sinnvoll, da type und id rein technische Auszeichnungen des Objekts sind und nicht mit den fachlichen Attributen vermischt werden sollten. Durch Weglassen aller anderen Attribute erhalten wir außerdem jederzeit die Objektreferenz {„id“: „1“, „type“: „teams“}.

 

Nach Datenobjekten suchen: Wo ist mein Team?

Für die Suche nach unseren Teams hängen wir wie gewohnt URL-Abfrageparameter an den URL-Pfad. Um nach Attributwerten zu filtern, sieht die Spezifikation Parameter mit dem Namensmuster filter[{attribute_name}] vor. Die Unterscheidung in den Attributen erfolgt über den assoziativen Parameter {attribute_name}. Die Suche nach dem Team „FC-Norden“ sieht dann beispielsweise folgendermaßen aus:

 

GET /teams?filter[name]=FC+Norden

Filterparameter lassen sich mit einem logischen UND verknüpfen:

GET /teams?filter[name]=FC+Norden+Jugend&filter[category]=juniors

oder können ein Set von Werten enthalten, was einem logischen ODER entspricht:

 

GET /teams?filter[category]=juniors,masters

 

Der reservierte Name filter hat den großen Vorteil, dass für den Anwender des API sofort klar wird, wozu der URL-Parameter verwendet wird.

 

Im Ergebnis blättern: vor und zurück …

Das Prinzip von assoziativen Parametern ist auch beim Blättern in der Ergebnisliste eine elegante Lösung: Zwei URL-Parameter, page[number] und page[size], genügen. Für den Anwender ist auch ohne Dokumentation die Bedeutung des Parameters sofort ersichtlich. Folgende Abfrage liefert Teams auf Seite drei mit maximal zehn Ergebnissen pro Seite (Listing 2).
 

Listing 2: Blättern in der Ergebnisliste

GET /teams?page[number]=3&page[size]=10 HTTP/1.1
Accept: application/vnd.api+json
  
{
   "data": [
   { /*...*/ },
   { /*...*/ },
   /*...*/
   ],
   "links": {
      "first": "http://example.com/teams?[number]=1&page[size]=10",
      "prev": "http://example.com/teams?page[number]=2&page[size]=10",
      "next": "http://example.com/teams?page[number]=4&page[size]=10"
      "last": "http://example.com/teams?page[number]=200&page[size]=10"
   }
}

Im Antwortdokument werden die Links für das Blättern geliefert. Die Angabe dazu findet sich im links-Element parallel zu data. Somit wird auch klar, warum es vorteilhaft ist, die eigentlichen Daten in einem eigenen data-Element zu liefern. Auf der obersten Ebene lassen sich zusätzlich weitere wichtige Daten an den Empfänger des Ergebnisses übermitteln. Diese Eigenschaft des JSON-API-Dokuments ist auch bei der Behandlung von Fehlern nützlich.

 

Ein neues Datenobjekt anlegen: Willkommen FC Oldenburg

Wie legen wir nun ein neues Team an? Auch hier kommt der API-Pfad /teams zum Einsatz. Allerdings verwenden wir die HTTP-Methode POST. Der Request sieht nun so aus wie in Listing 3 dargestellt.

Listing 3

  POST /teams HTTP/1.1  
  Content-Type: application/vnd.api+json  
  Accept: application/vnd.api+json  
  {  
     "data": {  
        "type": "teams",  
        "attributes": {  
           "name": "FC Oldenburg",  
           "category": "seniors"
        }
     }
}

Wieder benutzen wir das Dokumentattribut data zur Übertragung der eigentlichen Teamattribute. Die id wird in diesem Beispiel vom Server vergeben und ist im Dokument daher nicht angegeben. Es stellt sich die Frage, warum die Angabe „type“: „teams“ nicht ebenfalls entfallen kann: Lässt sich der type nicht ohnehin aus dem API-Pfad /teams ableiten? Das ist nur bedingt der Fall. Handelt es sich bei der Angabe im Pfad nämlich um einen abstrakten Basistyp, der verschiedene polymorphe Unterklassen kennt, dann benötigen wir für die Erzeugung einen konkreten (nicht abstrakten) Datentyp. Um also robuster gegen spätere Erweiterungen im Datenmodell zu sein, definieren wir das Feld type daher von vorn herein als zwingend. Als Antwort auf den POST Request wird das angelegte Team als Dokument geliefert (Listing 4).

Listing 4

{
   "data": {
      "type": "teams",
      "id": "3",
      "attributes": {
         "name": "FC Oldenburg",
         "category": "seniors"
      }    
      "links": {
         "self": "http://example.com/teams/3"
      }
   }
}

Die id wurde vom Server vergeben und ist nun ebenfalls im Dokument enthalten. Ein Link zur Ressource selbst wird im Feld self zurückgegeben.

HTTP-Methode POST

Wichtig zu wissen ist, dass ein POST per Definition den Status in einer Domäne verändert und somit nicht idempotent ist. Jeder POST führt zu einem neuen Ergebnis. Der Request POST /teams HTTP/1.1 erzeugt also jedes Mal eine neue Mannschaft. Soll ein Datenobjekt geändert werden, darf niemals POST, sondern es muss stattdessen PATCH oder PUT verwendet werden.

 

Ein Datenobjekt lesen: Zeig mir das Team mit der Nummer 3

Um ein einzelnes Datenobjekt zu lesen, wird ein API-Pfad verwendet, der dem Muster /{entity-type}/{entity-id} entspricht. Der Pfad wird also noch um die id der Entität erweitert. Es handelt sich dabei um den eindeutigen URI der zugehörigen Ressource. Der self-Link im vorangegangenen Beispiel entspricht genau solch einem URI. Das Team mit der Nummer 3 wird also mit einem einfachen HTTP GET gelesen:

 

GET /teams/3 HTTP/1.1
Accept: application/vnd.api+json

 

Als Antwort wird das Dokument mit der Referenz {„type“:“teams“, „id“:“3″} geliefert.

 

Ein Datenobjekt ändern: FC Essen ist gealtert …

Um ein Team zu ändern, benutzen wir wieder den URI der Ressource nach dem Muster /{entity-type}/{entity-id}. Mit der Methode PATCH können einzelne Attribute geändert werden, ohne das gesamte Objekt zu überschreiben (Listing 5).

Listing 5

PATCH /teams/2 HTTP/1.1
Accept: application/vnd.api+json
{
 "data": {
   "id": "2"
   "type": "teams",
   "attributes": {
    "category": "seniors"
   }
 }
}

Wir ändern lediglich das Attribut category, das Attribut name hingegen bleibt unverändert. Als Response wird das gesamte (geänderte) Dokument zurückgeliefert.
Eine HTTP-PATCH-Anfrage ist idempotent. Jeder Aufruf mit den gleichen Daten führt zum gleichen Status in der Domäne. Ein wiederholter, gleichlautender Aufruf ändert keine Daten am Server bzw. in unserer Datenbank.

 

Ein Datenobjekt löschen: FC Oldenburg gibt’s nicht mehr

Um ein Team zu löschen, verwenden wir, genau wie beim Lesen und Ändern, den URI der Ressource nach dem Muster /{entity-type}/{entity-id}. Als HTTP-Methode setzen wir jetzt einfach DELETE ein:

 

DELETE /teams/3 HTTP/1.1

 

Als Antwort erhalten wir einen entsprechenden HTTP-Statuscode, also 200 OK, wenn das Löschen erfolgreich war, oder 404 Not Found, wenn das zu löschende Objekt nicht existiert.

 

Der Software Architecture Track auf der W-JAX 2018

 

Über Beziehungen: Wer gehört zu wem?

Aus dem Datenmodell für unser Beispiel geht hervor, dass einem Team ein Manager und mehrere Spieler zugeordnet werden können. Sowohl Manager als auch Spieler sind vom Typ persons. Um den Manager und die Spieler eines Teams über das API verwalten zu können, wird der Begriff Relationship eingeführt. Das folgende Beispiel zeigt, wie die Beziehung manager im Element relationships abgebildet wird (Listing 6).

Listing 6: Beziehung „manager“-„relationships“

GET /teams/3 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
  "id:": "3"
  "type": "teams",
  "attributes": {
   "name": "FC Oldenburg"
},
  "relationships": {
   "manager": {
            "data": { "type": "persons", "id": "55" },
            "links": {
               "self": "http://example.com/teams/3/relationships/manager",
               "related": "http://example.com/teams/3/manager"
            }
         }
      }
   }
}

 

Beziehungen werden im Datenobjekt im Element relationships geliefert. Jede Relationship wird aus Sicht der verweisenden Entität über einen eindeutigen Namen identifiziert und enthält die Elemente data und links. Das data-Element kann ein oder mehrere Datenobjekte enthalten, je nach Kardinalität der Beziehung. Im konkreten Beispiel handelt es sich beim Manager um eine 1:1-Beziehung, es wird daher genau eine Referenz auf das Personen-Datenobjekt des Managers geliefert. Das links-Element enthält zwei Referenzen: Über den self-Link kann die Beziehung selbst verwaltet werden. Das Muster für den API-Pfad lautet hier: /{entity-type}/{entity-id}/relationship/{relationship-name}.
Wichtig zu erkennen ist hier, dass über den relationship-Link lediglich die Beziehung angesprochen wird. Der Link http://example.com/teams/3/relationships/manager liefert also die Relationship mit dem Namen manager, nicht das Dokument der referenzierten Person. Dieses wiederum bekommen wir mittels des zweiten Links mit dem Namen related. Der Link http://example.com/teams/3/manager liefert das Dokument mit den Daten zur Person. Auch hier sieht man, dass sich der type im Dokument nicht immer aus dem Request-URL ableiten lässt.

Die 1:1-Beziehung: Unser Team braucht einen Trainer!

Für das Team mit der Nummer 3 soll nun der Manager mit der Nummer 55 gesetzt werden. Für das Schreiben einer Beziehung kommt die HTTP-Methode PATCH zum Einsatz:

PATCH /teams/3/relationships/manager HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
   "data": { "type": "persons", "id": "55" }
}

Eine Beziehung, sofern sie optional ist, kann auch gelöscht werden. Das data-Element wird dazu auf null gesetzt:

PATCH /teams/3/relationships/manager HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
   "data": null
}

 

Die 1:n-Beziehung: Wer spielt mit?

Ähnlich verhält es sich mit der Verwaltung der Spielerbeziehungen. Im Unterschied zum Teammanager handelt es sich hier allerdings um eine 1:n-Beziehung zwischen Mannschaft und Spielern. Wir haben die Wahl zwischen HTTP-PATCH und –POST (Listing 7).

 

Listing 7

PATCH /teams/3/relationships/players HTTP/1.1
{
   "data": [
      { "type": "persons", "id": "10" },
      { "type": "persons", "id": "11" },
      { "type": "persons", "id": "12" },
      { "type": "persons", "id": "13" }
   ]
}
  
POST /teams/3/relationships/players HTTP/1.1
{
   "data": [
      { "type": "persons", "id": "17" },
      { "type": "persons", "id": "18" }
   ]
}

 

Der Unterschied ist auf den ersten Blick nicht gleich ersichtlich, wird aber schnell klarer: PATCH ersetzt die komplette Beziehung, also alle Spieler unserer Mannschaft. POST hingegen fügt die angegebenen Personen zu den bereits bestehenden Spielerbeziehungen hinzu, sofern sie noch nicht existieren. Analog können eine oder mehrere Spielerbeziehungen über ein HTTP-DELETE wieder gelöscht werden.

DELETE /teams/3/relationships/players HTTP/1.1
{
"data": { "type": "persons", "id": "10" }
}

Auch beim DELETE kann das data-Element wieder eine einzelne Referenz oder ein Array, eine Liste von Referenzen, enthalten.

Zugehörige Objekte einbinden: Ich will alles sehen!

Mit den bisher gezeigten Möglichkeiten wäre es sehr umständlich, alle Daten für ein bestimmtes Team, also die gesamte Mannschaft inklusive Trainer- und Spielernamen, zu laden. Zunächst müsste man das Teamobjekt lesen. Daraufhin müsste der Entwickler für jede Relation einen weiteren API-Aufruf absetzen, um die zugehörigen Personenobjekte zu laden. Das wäre weder performant, noch wäre der Spaß beim Programmieren sehr groß. JSON API spezifiziert hier eine elegante Möglichkeit, mit einem einzigen API-Aufruf alle benötigten Beziehungsobjekte gleich mitzuladen. Der URL-Parameter include ist dafür reserviert. Soll zum Team auch die Person zur Beziehung manager geladen werden, setzen wir als URL-Parameter ?include=manager (Listing 8).

 

Listing 8: Beziehungsobjekte laden

GET /teams/3?include=manager HTTP/1.1
Accept: application/vnd.api+json
  
{
   "data": {
      "id:": "3"
      "type": "teams",
      "attributes": {
         "name": "FC Oldenburg"
      },
      "relationships": {
         "manager": {
            "data": { "type": "persons", "id": "55" },
            "links": {
               "self": "http://example.com/teams/3/relationships/manager",
               "related": "http://example.com/teams/3/manager"
            }
         }
      }
   },
   "included": [
      {
         "id:": "55"
         "type": "persons",
         "attributes": {
            "name": "Coach Maier"
         }
      }
   ]
}

 

Die Daten des Trainers werden nun im Element included parallel zum Element data geliefert. Analog lassen sich auch die Spieler mit ein- und demselben Aufruf anfordern (Listing 9).

 

Listing 9: Spieler anfordern

GET /teams/3?include=manager,players HTTP/1.1
Accept: application/vnd.api+json
  
{
   "data": {
      "id:": "3", "type": "teams",
      "attributes": { "name": "FC Oldenburg" },
      "relationships": {
         "manager": {
            "data": { "type": "persons", "id": "55" },
            "links": { "related": "http://example.com/teams/3/manager" }
         },
         "players": {
            "data": [
               { "type": "persons", "id": "10" },
               { "type": "persons", "id": "11" }
            ],
            "links": { "related": "http://example.com/teams/3/players" }
         }
      }
   },
   "included": [
      {
         "id:": "55", "type": "persons",
         "attributes": { "name": "Coach Maier" }
      },
      {
         "id:": "10", "type": "persons",
         "attributes": { "name": "Johnny Wirbelwind" }
      },
      {
         "id:": "11", "type": "persons",
         "attributes": { "name": "Franz Luftikus" }
      }
   ]
}

 

Der include-Parameter eignet sich idealerweise auch bei der Suche, also der Abfrage einer Liste von Datenobjekten:

GET /teams?include=manager

 

Die include-Methode und die zugehörige Ergebnisstruktur haben ein paar wesentliche Vorteile gegenüber herkömmlichen Ansätzen. Die Daten der einzelnen Objekte bleiben streng getrennt. Ein inkludiertes Objekt ist nur einmal im Dokument vorhanden, auch wenn es mehrfach referenziert wird. Schließlich wird auch vermieden, dass das API speziell auf die Anforderungen verschiedener API-Konsumenten zugeschnitten werden muss (z. B. /teams/readAllWithManager?id=3 oder ähnliche Auswüchse).

Hier gilt es zu beachten, dass serverseitig ein solcher Request zu sehr aufwändigen Datenbankabfragen führen kann. In unserem Beispiel würde etwa die Anfrage /teams?include=manager,players die gesamte Mannschaftenliste inklusive aller Trainer- und Spielerdaten ausliefern. Denkt man kurz über die entsprechende SQL-Datenbankabfrage mittels Joins nach, kann man erahnen, dass das bei großen Datenmengen nicht unbedingt ideal ist. Es kann also sinnvoll sein, das Inkludieren bestimmter Beziehungen nicht automatisch für alle API-URL-Pfade zu erlauben. Wollen wir lediglich die Spieler eines Teams in einer Anfrage lesen, kann wieder der self-Link aus dem Relationship-Element players seinen Dienst tun. Der Link aus dem Beispiel http://example.com/teams/3/players liefert als Antwort alle Objekte vom Typ persons aus der Beziehung players.

Aktionen ausführen: Mach etwas!

Bisher wurden die klassischen Aktionen wie Create, Read, Update und Delete vorgestellt. Genügen diese Basisoperationen nicht, muss das API entsprechend erweitert werden. Eine Aktion (wie ’suspend player from team‘) führt meist auch ein Verb im Namen der Operation. Das REST-Design-Pattern empfiehlt hingegen, keine Verben im URL zu verwenden. Wie lassen sich weitere Serveroperationen in das API mit aufnehmen? Die JSON-API-Spezifikation geht auf Aktionen im API nicht näher ein. An einem Beispiel stellen wir ein paar Ansätze vor: Ein Spieler verletzt sich, und der Teammanager soll per Nachricht darüber informiert werden. Die entsprechende Methode steht bereits serverseitig zur Verfügung und soll nun in das API aufgenommen werden.

Action Verb: Ruf mich auf!

In der ersten Variante wird die Aktion in den Pfad aufgenommen: /players/1/notifyInjury. Die Verwendung des Verbs notify deutet auf eine Art Remote Procedure Call hin – etwas, das wir beim RESTful-Ansatz eigentlich vermeiden wollen. Jedenfalls handelt es sich bei notifyInjury nicht um eine Ressource im herkömmlichen Sinn. Es ist völlig unklar, welche der HTTP-Methoden GET, POST oder PATCH auf diese Pseudoressource angewandt werden sollen. Diese Methode ist daher nicht zu empfehlen.

Action Patch: Reagiere auf meine Änderung!

Soll das Verb nicht im API-Pfad vorkommen, lässt sich die serverseitige Methode auch durch einen einfachen PATCH auslösen (Listing 10).

Listing 10

PATCH /players/1 HTTP/1.1
{
   "data": {
      "id": "1"
      "type": "players",
      "attributes": {
         "condition": "injured"
      }
   }
}

 

Am Server werden Updates auf das Attribut condition entsprechend überwacht, und bei einer Änderung des Status wird der Teammanager über die Verletzung seines Spielers informiert. Die serverseitige Action muss bei dieser Variante idempotent implementiert werden: Der Manager darf nur bei einer Änderung des Status benachrichtigt werden, nicht bei jedem PATCH auf diese Ressource.

Action Metadaten: Schick mir einen Hinweis!

Eine weitere Möglichkeit bietet das (im JSON API spezifizierte) meta-Element (Listing 11).

 

Listing 11

POST /players/1 HTTP/1.1
{
   "data": {
      "id": "1", "type": "players"
   },
   "meta": {
   "action": "notifyInjury"
   }
}

 

Ein POST auf eine Ressource wird hier als Senden einer Aktion interpretiert. Die auszuführende Aktion wird dabei im Block meta als action mitgegeben. Zu beachten ist, dass ein POST nicht idempotent ist. Sollte der Spieler bereits verletzt sein, muss der Server mit einem Fehler reagieren. Der Vorteil dieser Variante: Kein zusätzlicher URL-Pfad wird definiert, sondern lediglich dem POST auf eine vorhandene Ressource eine Bedeutung zugeordnet.

Action Queue: Schick mir deine Aktion!

Will man die Aktion dennoch im API-Pfad führen, besteht die Möglichkeit, eine Action Queue [4] zu definieren. Der URL /players/1/actions bietet die Möglichkeit neue Actions (also Ressourcen vom Typ actions) wie gewohnt mittels POST über das API anzulegen. Optional könnten hier sogar vergangene Actions mittels GET ausgelesen werden. Der Vorteil dieser Variante: Eine Aktion ist eine (Pseudo-)Ressource und entspricht dem REST-Pattern. Der Nachteil: Es wird ein zusätzlicher Pfad im URL benötigt.

Fehlerbehandlung: Was läuft hier schief?

Statuscodes dienen dazu, den Schnittstellenbenutzer über den Ausgang der Request-Verarbeitung zu informieren. Das HTTP-Protokoll gibt Auskunft über mögliche Ergebnisse (Tabelle 1).

 

Statusgruppe Bedeutung2xx Verarbeitung ok3xx Umleitung4xx Fehlerhafte API-Bedienung5xx Serverseitiger FehlerTabelle 1: HTTP-Statuscodes

 

Die JSON-API-Spezifikation bietet die Möglichkeit, zusätzlich zum HTTP-Statuscode auch Fehlermeldungen im Element errors zu liefern (Listing 12).

Listing 12

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
   "errors": [
      {
         "code": "100",
         "status": "400",
         "source": {"pointer": "data.attributes.name"},
         "title":   "Mandatory field",
         "detail": "Attribute ‘name‘ must not be empty"
      }
   ]
}

Hier wird auf eine fehlerhafte Bedienung des API durch den Client hingewiesen. Die Antwort enthält kein data-Element und liefert stattdessen neben dem HTTP-Status 400 (Bad Request) eine oder mehrere Fehlermeldungen mit weiteren Details. Die JSON-API-Spezifikation nennt für ein Error-Objekt noch weitere Attribute [5]. Den Einsatz von HTTP-Statuscodes sollte man nicht übertreiben. Auf HTTP-Ebene genügen in der Regel ein paar wenige Codes. Eine detaillierte Beschreibung liefert ohnehin das errors-Element des Dokuments.

 

Free: Mehr als 40 Seiten Java-Wissen


Lesen Sie 12 Artikel zu Java Enterprise, Software-Architektur und Docker und lernen Sie von W-JAX-Speakern wie Uwe Friedrichsen, Manfred Steyer und Roland Huß.

Dossier herunterladen!

 

Datentypen: Was bist du?

Auf die Verwendung von Datentypen geht die JSON-API-Spezifikation kaum ein. An dieser Stelle sei lediglich erwähnt, dass die Definition von unterstützten Typen für Attributwerte unbedingt Teil eines Richtlinienkatalogs sein sollte. Einheitliche Formate und die entsprechende JSON-Darstellung für Datum, Zeitstempel oder auch eigene Klassen wie Money und Ähnliches sollten unbedingt im Vorfeld festgelegt werden.

API-Versionierung: Welchen Dialekt sprichst du?

Der erste Schritt ist schnell gemacht. Ein API wurde entworfen und ist im Einsatz. Ab diesem Zeitpunkt sollte die Schnittstelle möglichst stabil gegenüber Änderungen sein. Zusätzliche Attribute führen in der Regel zu keinen Verwerfungen mit den Konsumenten der Schnittstelle. Clients sollten gegenüber solchen Anpassungen robust implementiert werden. Was aber, wenn sich am Schema des API Grundlegendes ändert? Sind andere Entwicklerteams oder auch externe Partner von der Änderung betroffen, kann die Synchronisierung aller notwendigen Tätigkeiten aufwendig und schwierig werden. Daher muss sich der API-Entwickler auch Gedanken über unterschiedliche Versionen eines API machen. Die JSON-Spezifikation bietet hierzu keine vordefinierte Lösung. Sofern man rückwärtskompatible, alte Aufrufe weiterhin unterstützen möchte (oder muss) bietet sich die URI-Versionierung als Lösungsansatz an:


/v1/teams
/1.0/teams
/v1.1/teams

 

Wer den RESTful-Ansatz ernst nimmt, dem muss hier allerdings bewusst sein, dass mit jeder Einführung einer neuen Version aus der Sicht von außen ein vollständiges, neues Datenmodell entsteht. Niemals sollte die Verlinkung der Ressourcen untereinander auf verschiedenen API-Versionen basieren. Für einen Client sieht es so aus, als ob es sich um unabhängig voneinander existierende Datenquellen mit unterschiedlichen Datensätzen handeln würde.
Zu den Vorteilen in diesem Zusammenhang zählen die Einfachheit in der Umsetzung (abgesehen von der Rückwärtskompatibilität) und die unkomplizierte Anwendung. Ein Nachteil ist, dass dieselbe Ressource unter mehreren Pfaden abrufbar ist. Der URL für ein und dieselbe REST-Ressource ist nicht für alle Zeiten fix, und Links in externen Systemen müssen eventuell angepasst werden.
Außerdem fehlt ein kanonischer Identifikator: Zwei unterschiedliche URIs können auf dieselbe dahinterliegende Resource verweisen, ohne dass dies dem Client bewusst ist. Für weitere Ansätze zum Thema Versionierung verweisen wir auf das Buch „Build APIs You Won’t Hate“ [6].

Fazit: Wir sind gewappnet

Ein starker Trend in der Softwareindustrie geht weg vom Monolith hin zu kleinen Domain- und Microservices. Diese Entwicklung erfordert stabile und gut durchdachte APIs, um nicht im Schnittstellenchaos zu versinken. Die JSON-API-Spezifikation bietet sehr viele und gute Lösungsansätze rund um das Thema RESTful JSON. Eigene Richtlinien lassen sich aus ihr leicht ableiten. Von Anfang an gut gegen den API-Wildwuchs gewappnet, steht einer erfolgreichen Lösung nichts mehr im Weg.

 

Links & Literatur
[1] JSON-API-Spezifikation: http://jsonapi.org
[2] Keywords for use in RFCs to Indicate Requirement Levels: https://www.ietf.org/rfc/rfc2119.txt
[3] JSON API/Document Structure: http://jsonapi.org/format/#document-structure
[4] Thoughts on RESTful API Design: http://restful-api-design.readthedocs.io/en/latest/methods.html#actions
[5] JSON API/Errors: http://jsonapi.org/format/#errors
[6] Sturgeon, Philip: „Build APIs You Won’t Hate“: https://leanpub.com/build-apis-you-wont-hate

Alle News der Java-Welt:
Alle News der Java-Welt:

Behind the Tracks of W-JAX 2018

Agile & Culture
Teamwork & Methoden

Big Data & Machine Learning
Speicherung, Processing & mehr

Clouds, Container & Serverless
Alles rund um Cloud

Core Java & JVM Languages
Ausblicke & Best Practices

DevOps & Continuous Delivery
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Web Development & JavaScript
JS & Webtechnologien

Performance & Security
Sichere Webanwendungen

Serverside & Enterprise Java
Spring, JDK & mehr

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Software Architecture
Best Practices