Wie RESTful ist deine Schnittstelle?

Wer heute zu Tage einen Web Service implementiert gibt ihm eine REST Schnittstelle. Aber REST ist nicht gleich REST! Leonard Richardson hat schon 2007 das REST Maturity Model (zu Deutsch etwas klobig REST-Reifegradmodell) mit 4 Stufen vorgestellt. Im Folgenden werde ich erläutern, wie die Stufen definiert sind und was sie für eine Schnittstellen-Architektur bedeuten. Zudem diskutiere ich, ob die Stufen so trennscharf sind wie es der Entwurf von Richardson suggeriert.

Das Rest Maturity Model (RMM) kennt, wie gesagt, 4 Stufen von 0 bis 3. Im Prinzip geht es darum, wie sehr man von den Möglichkeiten des verwendeten Transfer-Protokolls (meistens HTTP) gebraucht macht. Wie Joshua Thijssen in seinem kurzen Artikel über das RMM schreibt, ist das aber nicht zwingend der Fall. Der Einfachheit halber bleibe ich bei HTTP.

Als Beispiel verwende ich die fiktive Buchungsschnittstelle eines Hotels über die ein Kunde freie Hotelzimmer suchen, buchen und stornieren kann. Dabei setze ich voraus, dass der Kunde registriert und angemeldet ist und die Session über ein token identifiziert wird.

Level 0

Auf Stufe 0 macht eine Schnittstelle gar keinen Gebrauch von den Fähigkeiten des Protokolls. Alle Requests gehen gegen einen Endpunkt und verwenden die selbe Protokoll-Variante (bei HTTP das Verb POST). XML_RPC und SOAP sind bekannte Vertreter dieses Levels weshalb diese Stufe den wenig schmeichelnden Spitznamen „Swamp of POX“ hat. POX steht dabei für „Plain Old XML“ oder seltener „POST Only XML“.

Ein Problem mit diesem Level ist, dass der Server das gesamte Routing zwischen den Teilen der Applikation vollständig selbst handhaben muss. Wer einmal ein WSDL-Dokument oder eine SOAP-Nachricht genauer betrachtet hat weiß, wie viele Ebenen von XML aufgebaut werden müssen, bevor man zum eigentlichen Inhalt kommt. Darüber hinaus muss explizit gesagt werden, ob es sich um eine Anfrage (Request) oder eine Antwort (Response) handelt. Dazu kommt, dass nicht zwischen lesenden und schreibenden Operationen unterschieden wird. Für unsere Buchungsschnittstelle brauchen wir also z.B. Operationen wie bookRoom, getBookings, changeBooking und cancelBooking; jeweils mit Request und Response.

(POST) https://myhotel.eu/api/v1/

<hotelapi>
	<bookRoom>
		<request>
			<token>abc123</token>
			<roomno>412</roomno>
			<arrival>2015-06-30T15:00:00+0200</arrival>
			<departure>2015-07-13T11:00:00+0200</departure>
		</request>
	</bookRoom>
<hotelapi>

(200)

<hotelapi>
	<bookRoom>
		<response>
			<booking>
				<bookingno>15.0123</bookingno>
				<roomno>412</roomno>
				<arrival>2015-06-30T15:00:00+0200</arrival>
				<departure>2015-07-13T11:00:00+0200</departure>
				<bookingPrice>1000 EUR</bookingPrice>
			</booking>
		</request>
	</bookRoom>
<hotelapi>

Zu guter Letzt bauen viele POX-Schnittstellen ihre Fehlerbehandlung ebenfalls. Bei SOAP kommt im Fehlerfall eine valide HTTP-Antwort mit Code 200 (OK) zurück, welche einen SOAP Fault enthält.

Level 0.5 (nanu?)

Auf dem Weg zu einer REST-Schnittstelle kann man den Zwischenschritt einlegen, für jede Operation einen Endpunkt zu definieren. Das vereinfacht häufig schon vieles und nutzt eine paar der impliziten Eigenschaften des Protokolls: Das token kann in den HTTP-Header und der Unterschied zwischen Request und Response ergibt sich in aller Regel auch aus dem Kontext. Unsere Buchungsanfrage sieht dann schon sehr viel schlanker aus:
(POST) https://myhotel.eu/api/v2/bookRoom/

<data>
	<roomno>412</roomno>
	<arrival>2015-06-30T15:00:00+0200</arrival>
	<departure>2015-07-13T11:00:00+0200</departure>
</data>

(200)

<booking>
	<bookingno>15.0123</bookingno>
	<roomno>412</roomno>
	<arrival>2015-06-30T15:00:00+0200</arrival>
	<departure>2015-07-13T11:00:00+0200</departure>
	<bookingPrice>1000 EUR</bookingPrice>
</booking>

Verwendet man statt XML z.B. JSON fallen die äußeren Elemente weg und die Sache wird noch ein bisschen übersichtlicher:

{ 
	"roomno" : "412",
	"arrival" : "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200"
}

Level 1

Auf Stufe 1 wird eine Schnittstelle zum ersten Mal dem ‚R‘ in REST gerecht: Die Schnittstelle kennt Ressourcen und kann diese adressieren. Üblicherweise gibt es für jeden Ressourcen-Typ einen eigenen Endpunkt. Statt anzugeben, welchen Raum man buchen möchte, teilt man dies dem Raum selbst mit.

(POST) https://myhotel.eu/api/v3/rooms/412/book/

{
	"arrival" : "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200"
}

(200)

{	
	"bookingno" : "15.0123",
	"roomno" : "412",
	"arrival": "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200",
	"bookingPrice" : "1000 EUR"
}

Eine Änderung der Buchung sähe dann etwa so aus:

(POST) https://myhotel.eu/api/v3/bookings/15.0123/change/

{
	"arrival": "2015-07-01T15:00:00+0200"
}

(200)

{	
	"booking" : {
		"bookingno" : "15.0123",
		"roomno" : "412",
		"arrival": "2015-07-01T15:00:00+0200",
		"departure" : "2015-07-13T11:00:00+0200",
		"bookingPrice" : "945 EUR"
	},
	"changeCost" : "5 EUR"
}

Level 2

Auf Stufe 2 kommen weitere Aspekt von HTML zum Tragen: Die HTML-Verben und die Status Codes. Wie Thijssen – zu Recht – kritisiert ist diese Stufe sehr auf HTTP als Transferprotokoll fixiert. HTTP definiert eine erstaunlich große Anzahl von Methoden. Obwohl ihre Semantik im RFC sehr klar beschrieben ist werden sie erstaunlich häufig falsch verwendet.

GET (und HEADER) dienen z.B. nur dazu, Informationen über eine Ressource abzurufen. Laut RFC darf z.B. GET keine Nebenwirkungen haben. POST hingegen dient dazu eine neue Ressource anzulegen.

The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line.

Im Falle unserer Buchung würde das bedeuten, dass wir entweder dem Raum eine Buchung geben oder der Liste der existierenden Buchungen eine für den Raum hinzufügen.

(POST) https://myhotel.eu/api/v4/rooms/412/bookings/

{ 
	"arrival" : "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200"
}

(201)

{	
	"bookingno" : "15.0123",
	"roomno" : "412",
	"arrival": "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200",
	"bookingPrice" : "1000 EUR"
}

Sieht fast genauso aus wie vorher, hat aber eine subtil andere Semantik. Der Status Code 201 sagt über ein „OK“ hinaus, dass eine Ressouce angelegt wurde. Weitere Unterschiede treten zu Tage, wenn die Kundin nur die Liste ihrer Buchungen abrufen will. Durch die Verwendung von GET wird deutlich signalisiert, dass der Aufruf nur Daten ausgibt und nichts verändert.

(GET) https://myhotel.eu/api/v4/bookings?status=open

(200)

{[{	
	"bookingno" : "15.0123",
	"roomno" : "412",
	"arrival": "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200",
	"bookingPrice" : "1000 EUR"
}]}

Die Methoden PUT und DELETE dienen dazu, eine existierende Ressource zu manipulieren oder zu löschen.

(PUT) https://myhotel.eu/api/v4/bookings/15.0123/

{
	"arrival": "2015-07-01T15:00:00+0200"
}

Nach der reinen Lehre müsste man die komplette Buchung übergeben; das halte ich aber für unnötig.

Zu guter Letzt kann die Buchung auch storniert werden:

(DELETE) https://myhotel.eu/api/v4/bookings/15.0123/

(204)

Das wars. Kein Body, keine große Response. Eine Response mit Status 204 darf nämlich keinen Body enthalten. Auch Status 200 wäre hier zulässig und denkbar, z.B. um Stornierungskosten auszugeben.

Level 2.5 (auch von mir)

Als Zwischenschritt zu Stufe 3 gibt es noch eine weitere Verbesserung: Hinfort mit den magic numbers!

Aktuell enthält die Buchung zwei Felder, deren Bedeutung man raten muss: bookingno und roomno. Statt dessen sollten die Felder deutlich anzeigen, was sie enthalten: Referenzen auf einen Raum und die Buchung selbst.

{[{	
	"self" : "/bookings/15.0123",
	"room" : "/rooms/412",
	"arrival": "2015-06-30T15:00:00+0200",
	"departure" : "2015-07-13T11:00:00+0200",
	"bookingPrice" : "1000 EUR"
}]}

Auf diese Weise ist sofort deutlich, um was für Ressourcen es sich handelt und wo diese gefunden werden können.

Level 3

Level 3 firmiert unter dem Akronym HATEOAS (Hypertext As The Engine Of Application State) und ist recht kompliziert. Hier nur soviel: Zusätzlich zu den Informationen über eine Ressource und ihrer Beziehung zu anderen Ressourcen gibt eine HATEOAS-Schnittstelle weitere Hinweise, wie mit ihr weiter verfahren wird. Martin Fowler geht in seiner Erläuterung des RMM stärker auf Level 3 ein. Ich selbst werde mich – wie die meisten anderen Autoren im Netz – darum drücken. 😀

Aber jetzt Hand aufs Herz: Wie RESTful ist denn nun deine Schnittstelle?

Titelbild By Rock1997 (Own work) [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC BY-SA 4.0-3.0-2.5-2.0-1.0 (http://creativecommons.org/licenses/by-sa/4.0-3.0-2.5-2.0-1.0)], via Wikimedia Commons

Teilen Sie diesen Beitrag

Das könnte dich auch interessieren …

4 Antworten

  1. Waldemar Biller sagt:

    Ist deine Stufe 2.5 nicht wirklich Stufe 3? Das sieht mir so nach Links aus 😉

    • Christoph Grimmer-Dietrich sagt:

      Nein, das ist noch nicht Stufe 3. Auf Stufe 3 gebe ich links mit weiteren Schritten und Möglichkeiten an. Ich würde sowas ausgeben wie
      {
      „self“ : „/bookings/1234“,
      „cancel“ : „/bookings/1234/cancel“,
      „change“ : „/bookings/1234/change“,
      ….
      }

      Ich gehe auf 2.5 nur weg von reinen Symbolen ohne explizite Semantik zu URIs. Allerdings kann man diesen Schritt auch irgendwo anders auf dem Weg einlegen. Wie ich am Anfang des Artikels geschrieben habe, sind die Stufen gar nicht so trennscharf wie sie auf den ersten Blick scheinen. Es ist mehr ein Spektrum zwischen POX und HATEOAS 🙂

  2. Waldemar Biller sagt:

    Achtung solche URLs (/bookings/1234/cancel) sind ganz sicher nicht ReSTful. Du umgehst damit die HTTP-Verben und reduzierst, wie die Web-Browser auch, das Web auf GET und POST.

    Ein Link muss _immer_ auf eine Ressource (/bookings/1234) zeigen! Deshalb warst du auch mit deinem Beispiel schon auf Level 3. Ob der Link-Kladaradatsch nun in einem eigenen Abschnitt steht oder nicht spielt hier keine Rolle.
    Welche Operationen ein Client auf einer Ressource ausführen darf, muss er über OPTIONS herausfinden.

    Bei deinen Gegenbspiel solltest du für „cancel“ sollte man DELETE verwenden. Bei „change“ empfiehlt sich PUT. Beide Verben erlauben die Verwendung eines Request Body. Nur für den Fall das du Teilstornierungen durchführen willst.

    • Christoph Grimmer-Dietrich sagt:

      Waldemar, du hast natürlich Recht damit, dass ich die HTTP-Verben damit ausheble. Mein Codebeispiel hätte wie folgt aussehen müssen:
      {
      “self” : “/bookings/1234″,
      “cancel” : “/bookings/1234”,
      „addBookee“ : /booking/1234/persons“,
      ….
      }

      Ich habe bei der Übertragung von Fowlers Beispiel die beiden Teile der Links vermischt 🙁

      Nichts desto trotz halte ich an der Trennung fest, dass die Ersetzung von magic numbers durch URIs noch nicht Level 3 ist sondern als Zwischenschritt (auch schon früher) in Betracht gezogen werden kann.

Schreibe einen Kommentar

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